Document version: v4 final consolidated Date: 2026-05-08 Scope: CachyOS · Ubuntu 26.04 · Alpine 3.23 (x86_64 + aarch64/Raspberry Pi) · Void Linux glibc · Void Linux musl
This document is intentionally not summarized. Every command, every patch, every gotcha, every example — included in full. If you have never built a Linux kernel before, you can follow this start-to-finish without needing other references. If you have built kernels for fifteen years, the sections you don't need will be obvious from the headings.
- What This Vulnerability Is
- The Two Patches Reproduced In Full
- Decision Tree — Do You Even Need This?
- Phase 0 — Universal Mitigation (Every Distro, 5 Minutes)
- Phase 0.5 — Pre-Flight Checks
- Phase 1 — CachyOS (kernel 7.0.x / 6.18.x LTS)
- Phase 2 — Ubuntu 26.04 (kernel 7.0)
- Phase 3 — Alpine Linux 3.23 x86_64 (kernel 6.18 LTS)
- Phase 4 — Alpine Linux 3.23 aarch64 / Raspberry Pi 4 & 5
- Phase 5 — Void Linux glibc (kernel 6.18 / 7.0)
- Phase 6 — Void Linux musl (kernel 6.18 / 7.0)
- Phase 7 — Container Host Hardening
- Phase 8 — Verification (Cross-Distro)
- Phase 9 — Cleanup When Distros Ship Official Fix
- Troubleshooting Reference
- Worked Examples
- Glossary
- References
There are two ways the Linux kernel can be tricked into writing data into your computer's RAM page cache for files you only have read access to. Both go through the network stack. Both involve splice(). Both end with the attacker getting root because they overwrote /usr/bin/su or /etc/passwd directly in the page cache, and the next execve() or PAM call honored the modified version.
CVE-2026-43284 (ESP) — An attacker creates a user namespace, gets CAP_NET_ADMIN inside it, registers an XFRM Security Association, then sends a UDP packet over loopback whose payload contains splice-pinned page cache pages. On receive, esp_input() takes the skip_cow fast path because the skb isn't cloned and has no frag_list — but it has externally-pinned frags from splice. The in-place AEAD decrypt writes 4 bytes of the attacker's seq_hi into the page cache at an attacker-controlled offset. Repeat 48 times to overwrite the ELF header of /usr/bin/su with shellcode that does setuid(0); execve("/bin/sh").
CVE-2026-43500 (RxRPC) — Same concept, different protocol. The attacker doesn't need user namespaces — socket(AF_RXRPC) and add_key("rxrpc", ...) are unprivileged. The crypto is pcbc(fcrypt) instead of AEAD, giving 8-byte writes. The value isn't directly controlled (it's fcrypt_decrypt(ciphertext, key)) so the attacker brute-forces the 56-bit fcrypt key in userspace until the decrypt output is the bytes they want. They target /etc/passwd, wiping the password hash field for root so PAM accepts a blank password. Three overlapping 8-byte writes turn "root:x:0:0:root:..." into "root::0:0:GGGG:...".
The chain: try ESP first (works everywhere except where unshare(CLONE_NEWUSER) is blocked). Fall back to RxRPC where ESP fails (works on Ubuntu where rxrpc.ko ships by default). Together they cover every major distro.
To understand why the fix looks the way it does, you need to understand three kernel subsystems and how they accidentally interact.
The Page Cache. When you cat /usr/bin/su, the kernel reads it once into RAM and serves subsequent reads from that cache. Every read(), mmap(), and execve() of that file sees the same physical memory. If you write into those pages in RAM, every subsequent access sees your modified version until the cache is dropped or the system reboots. The disk file is untouched, but the in-memory view is corrupted. This is the prize.
splice() and zero-copy networking. splice() moves data between file descriptors without copying through userspace. splice(file_fd → pipe → socket) doesn't copy the file's data into a new buffer — it takes the actual page cache page (the same physical RAM page backing /usr/bin/su) and hands a reference to it directly to the socket's send buffer (the sk_buff). The page ends up in the skb's frag[] array. The critical assumption is that the networking code will only read from these pages, never write, because the pages belong to the page cache.
In-place crypto. IPsec ESP and RxRPC both decrypt in-place as a performance optimization. Instead of allocating a new buffer and copying data there, they decrypt right where the bytes sit in the skb — both src and dst of the crypto operation point to the same memory. This is fine when the skb owns its pages privately. It is catastrophically wrong when the pages are borrowed page cache via splice.
The bug is a violation of an unwritten contract. splice says "here's a page, you can read it but don't write." The crypto code says "I'm going to decrypt in-place, which means writing." When these two meet on the same skb, the attacker gets a write primitive into the page cache of any file they can read.
Dirty Pipe corrupted the page cache through the pipe subsystem. The fix was a one-liner in copy_page_to_iter_pipe(). Dirty Frag is the same end result through a different mechanism (skb frags + crypto). The shared element is splice() — it's the bridge that moves page cache pages into contexts where they can be inappropriately modified. The researcher (Hyunwoo Kim) explicitly calls this a "bug class": anywhere splice-originated pages end up in code that writes to them in-place, you have a vulnerability. Copy Fail (CVE-2026-31431) was the algif_aead instance. Dirty Frag is the esp4/esp6/rxrpc instances. There may be more.
This is the patch you save to disk as dirtyfrag-esp-fix.patch. It's based on upstream f4c50a4034e6 (Kuan-Ting Chen) with one local modification: an additional !skb->data_len check in the skip_cow branch. The local check exists because upstream only tags frags from __ip_append_data and __ip6_append_data — but pages can also reach skb frags via AF_PACKET TX_RING, vhost_net zerocopy, and BPF helpers, none of which set SKBFL_SHARED_FRAG. The data_len check catches all non-linear skbs regardless of how the frags arrived.
From f4c50a4034e62ab75f1d5cdd191dd5f9c77fdff4 Mon Sep 17 00:00:00 2001
Subject: [PATCH 1/2] esp,ip: fix page-cache write via splice + in-place crypto (CVE-2026-43284)
Based on upstream f4c50a4034e6 (Kuan-Ting Chen, Cc: stable) with a local
belt-and-suspenders addition: !skb->data_len in the skip_cow branch.
Upstream tags splice frags with SKBFL_SHARED_FRAG in ip_output/ip6_output
and checks the flag in esp_input/esp6_input. This is correct but only as
complete as the set of source-side taggings — AF_PACKET TX_RING, vhost_net
zerocopy, and BPF helpers can also inject external pages into skb frags
without setting the flag. Adding !skb->data_len catches ALL non-linear
skbs regardless of origin, mirroring the RxRPC fix approach.
Cost: kernel-owned NIC scatter-gather frags now take skb_cow_data() too.
Immeasurable on desktop/workstation. On a 10Gbps IPsec gateway, 5-15%
throughput regression on non-linear RX — significant, evaluate before
deploying on dedicated gateways.
NOTE ON LINE NUMBERS: These hunks target kernel ~6.13 source layout.
Current kernels (7.0.x, 6.18.x) have shifted line numbers due to
upstream changes. Apply with: patch -Np1 --fuzz=5
If fuzz fails, apply manually — changes are 1-2 lines per file.
NOTE ON MSG_NO_SHARED_FRAGS: Verify this symbol exists in your kernel
tree BEFORE applying. Run: grep -rn MSG_NO_SHARED_FRAGS include/
If absent, edit this patch to remove the `if (!(flags & ...))` guard
and just always set the flag.
Fixes: cac2661c53f3 ("esp4: Avoid skb_cow_data whenever possible")
Fixes: 03e2a30f6a27 ("esp6: Avoid skb_cow_data whenever possible")
Fixes: 7da0dde68486 ("ip, udp: Support MSG_SPLICE_PAGES")
Fixes: 6d8192bd69bb ("ip6, udp6: Support MSG_SPLICE_PAGES")
Cc: stable@vger.kernel.org
Reported-by: Hyunwoo Kim <imv4bel@gmail.com>
Signed-off-by: Kuan-Ting Chen <h3xrabbit@gmail.com>
---
net/ipv4/esp4.c | 4 +++-
net/ipv4/ip_output.c | 2 ++
net/ipv6/esp6.c | 4 +++-
net/ipv6/ip6_output.c | 2 ++
4 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/net/ipv4/esp4.c b/net/ipv4/esp4.c
index 6dfc0bcde..100000001 100644
--- a/net/ipv4/esp4.c
+++ b/net/ipv4/esp4.c
@@ -873,7 +873,9 @@ static int esp_input(struct xfrm_state *x, struct sk_buff *skb)
nfrags = 1;
goto skip_cow;
- } else if (!skb_has_frag_list(skb)) {
+ } else if (!skb_has_frag_list(skb) &&
+ !skb_has_shared_frag(skb) &&
+ !skb->data_len) {
nfrags = skb_shinfo(skb)->nr_frags;
nfrags++;
diff --git a/net/ipv4/ip_output.c b/net/ipv4/ip_output.c
index e4790cc7b..5bcd73cbd 100644
--- a/net/ipv4/ip_output.c
+++ b/net/ipv4/ip_output.c
@@ -1233,6 +1233,8 @@ static int __ip_append_data(struct sock *sk,
if (err < 0)
goto error;
copy = err;
+ if (!(flags & MSG_NO_SHARED_FRAGS))
+ skb_shinfo(skb)->flags |= SKBFL_SHARED_FRAG;
wmem_alloc_delta += copy;
} else if (!zc) {
int i = skb_shinfo(skb)->nr_frags;
diff --git a/net/ipv6/esp6.c b/net/ipv6/esp6.c
index 9f7531373..100000002 100644
--- a/net/ipv6/esp6.c
+++ b/net/ipv6/esp6.c
@@ -915,7 +915,9 @@ static int esp6_input(struct xfrm_state *x, struct sk_buff *skb)
nfrags = 1;
goto skip_cow;
- } else if (!skb_has_frag_list(skb)) {
+ } else if (!skb_has_frag_list(skb) &&
+ !skb_has_shared_frag(skb) &&
+ !skb->data_len) {
nfrags = skb_shinfo(skb)->nr_frags;
nfrags++;
diff --git a/net/ipv6/ip6_output.c b/net/ipv6/ip6_output.c
index 7e92909ab..1f2a33fbe 100644
--- a/net/ipv6/ip6_output.c
+++ b/net/ipv6/ip6_output.c
@@ -1794,6 +1794,8 @@ static int __ip6_append_data(struct sock *sk,
if (err < 0)
goto error;
copy = err;
+ if (!(flags & MSG_NO_SHARED_FRAGS))
+ skb_shinfo(skb)->flags |= SKBFL_SHARED_FRAG;
wmem_alloc_delta += copy;
} else if (!zc) {
int i = skb_shinfo(skb)->nr_frags;
--
2.45.0
What this patch actually changes:
In net/ipv4/esp4.c and net/ipv6/esp6.c, the esp_input() function had three branches for deciding whether to skip the copy-on-write step before in-place crypto:
- Linear skb, not cloned → skip cow (safe, bytes are kernel-owned)
- Has frags but no frag_list → skip cow (VULNERABLE if frags are splice-pinned)
- Anything else → call
skb_cow_data()to copy into a private buffer
The patch adds two new conditions to branch 2: only skip cow if !skb_has_shared_frag(skb) AND !skb->data_len. The first is the upstream check (matches Chen's commit). The second is our local hardening.
In net/ipv4/ip_output.c and net/ipv6/ip6_output.c, the __ip_append_data() function gets a new line that tags the skb's shinfo->flags with SKBFL_SHARED_FRAG whenever the datagram append path processes splice-originated pages. This is the "source side" tagging that the ESP "sink side" check looks for.
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
Subject: [PATCH 2/2] rxrpc: fix page-cache write via splice + in-place decrypt (CVE-2026-43500)
Add || skb->data_len to the gate in call_event.c and conn_event.c so
that non-linear skbs (data_len > 0) are routed through skb_copy()
before in-place pcbc(fcrypt) decryption.
The existing code only checked skb_cloned(skb), which misses
splice-pinned skbs — they are not cloned, just non-linear.
ALTERNATIVE: If you do not use kAFS (almost nobody does), simply
blacklist rxrpc.ko instead of applying this patch:
echo 'install rxrpc /bin/false' > /etc/modprobe.d/blacklist-rxrpc.conf
This patch was submitted to netdev on 2026-04-29 but has NOT been
merged upstream as of 2026-05-08.
NOTE ON LINE NUMBERS: These hunks target kernel ~6.13 source layout.
Apply with --fuzz=3.
Reported-by: Hyunwoo Kim <v4bel@theori.io>
Signed-off-by: Hyunwoo Kim <v4bel@theori.io>
---
net/rxrpc/call_event.c | 2 +-
net/rxrpc/conn_event.c | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/net/rxrpc/call_event.c b/net/rxrpc/call_event.c
index fdd683261226..6c924ef55208 100644
--- a/net/rxrpc/call_event.c
+++ b/net/rxrpc/call_event.c
@@ -334,7 +334,7 @@ bool rxrpc_input_call_event(struct rxrpc_call *call)
if (sp->hdr.type == RXRPC_PACKET_TYPE_DATA &&
sp->hdr.securityIndex != 0 &&
- skb_cloned(skb)) {
+ (skb_cloned(skb) || skb->data_len)) {
/* Unshare the packet so that it can be
* modified by in-place decryption.
*/
diff --git a/net/rxrpc/conn_event.c b/net/rxrpc/conn_event.c
index a2130d25aaa9..eab7c5f2517a 100644
--- a/net/rxrpc/conn_event.c
+++ b/net/rxrpc/conn_event.c
@@ -245,7 +245,7 @@ static int rxrpc_verify_response(struct rxrpc_connection *conn,
{
int ret;
- if (skb_cloned(skb)) {
+ if (skb_cloned(skb) || skb->data_len) {
/* Copy the packet if shared so that we can do in-place
* decryption.
*/
--
2.45.0
What this patch actually changes:
The original code only copies the skb before in-place decrypt if skb_cloned(skb) — meaning another reference exists to the same skb data. But a splice-pinned skb is NOT cloned — it's just non-linear with frags. skb->data_len > 0 means the skb has data outside the linear head buffer (in frags[] or frag_list). Adding || skb->data_len catches all non-linear skbs, routing them through skb_copy() which allocates a fresh linear buffer.
The RxRPC fix takes the broader approach because rxrpc isn't performance-sensitive (kAFS only, used by almost nobody) and the bytes involved are tiny. The ESP fix uses SKBFL_SHARED_FRAG because IPsec is performance-sensitive and they wanted to preserve the fast path. Both approaches are correct; they just trade off precision vs. defense-in-depth differently.
# In whatever working directory you'll be operating from:
mkdir -p ~/dirtyfrag-patches
cd ~/dirtyfrag-patches
# Save dirtyfrag-esp-fix.patch with the contents from section 2.1
nano dirtyfrag-esp-fix.patch
# (paste, save, exit)
# Save dirtyfrag-rxrpc-fix.patch with the contents from section 2.2
nano dirtyfrag-rxrpc-fix.patch
# Verify they look right
ls -la
# -rw-r--r-- 1 user user 2473 May 8 14:32 dirtyfrag-esp-fix.patch
# -rw-r--r-- 1 user user 1389 May 8 14:33 dirtyfrag-rxrpc-fix.patchBefore you do anything else, run through this tree. Most readers will exit it within five minutes with no kernel rebuild required.
┌─ Step 1: Are you running an actively-maintained distro that picks up upstream
│ stable backports?
│ ├─ YES (CachyOS, Ubuntu LTS, Alpine, Debian stable, Fedora) →
│ │ Chen's ESP patch is Cc: stable. It propagates automatically.
│ │ By kernel build dates of May 2026+, the patch is likely already in.
│ │ Continue to Step 2.
│ └─ NO (custom kernel, ancient distro, embedded) →
│ You must patch manually. Skip to Step 3.
│
├─ Step 2: Apply Phase 0 mitigation (5 min, no reboot), then run --check
│ to see if the patch is already in your kernel source.
│ ├─ Patch already present → Run --verify, remove blacklist, you're done.
│ └─ Patch not present → Continue to Step 3.
│
├─ Step 3: Do you actually USE IPsec ESP (strongSwan, Libreswan, kernel xfrm)?
│ ├─ NO (you use WireGuard, Tailscale, or no VPN) →
│ │ Module blacklist is your complete fix. Stop.
│ └─ YES (you have an IPsec tunnel that must keep working) →
│ You need a real kernel patch. Continue to Step 4.
│
├─ Step 4: Do you use kAFS (Andrew File System)?
│ ├─ NO (you don't — almost nobody does) →
│ │ Blacklist rxrpc.ko, apply only the ESP patch.
│ └─ YES →
│ Apply both patches. Use --rxrpc flag in the build scripts.
│
└─ Step 5: Build a custom kernel for your distro. Skip to your distro's phase.
Key data points to inform your decision:
- Chen's ESP patch (
f4c50a4034e6) merged upstream in late April 2026 withCc: stable@vger.kernel.org - The stable kernel team backports all
Cc: stablepatches to active stable trees - CachyOS 7.0.3-1.1 was built May 2, 2026 — almost certainly includes the patch
- Alpine 3.23 ships kernel 6.18 LTS, which is on the active stable tree
- Ubuntu 26.04 ships kernel 7.0; SRU pipeline typically lands stable backports within 1-2 weeks
- The RxRPC patch is NOT merged upstream as of this writing
The realistic expected outcome for 80%+ of readers running production distros: blacklist now, run check, find the patch is already in, verify, remove blacklist, total time ~30 minutes.
This works on every Linux distribution. It does not require a reboot. It does not modify the kernel. It just tells modprobe to refuse to load the vulnerable modules.
When something tries to load esp4.ko (e.g., when you run ip xfrm state add ... for the first time, or when systemd-networkd brings up an IPsec tunnel), the kernel calls modprobe. modprobe reads /etc/modprobe.d/*.conf and processes the install directive, which says "instead of loading this module, run this command." Setting that command to /bin/false means the module never loads, regardless of who asked.
This blocks both vulnerabilities at the only point they can be exploited: module load time. The vulnerable code never gets into RAM.
sudo tee /etc/modprobe.d/dirtyfrag-mitigation.conf > /dev/null << 'EOF'
# Dirty Frag mitigation: CVE-2026-43284 (ESP) + CVE-2026-43500 (RxRPC)
# Remove this file once running a kernel with both fixes verified.
install esp4 /bin/false
install esp6 /bin/false
install rxrpc /bin/false
install ipcomp4 /bin/false
install ipcomp6 /bin/false
install xfrm_user /bin/false
EOF# Try to unload each module (won't error if not loaded, will fail if in use)
for m in esp4 esp6 rxrpc ipcomp4 ipcomp6 xfrm_user; do
if lsmod | grep -q "^$m"; then
sudo modprobe -r "$m" 2>/dev/null \
&& echo "Unloaded $m" \
|| echo "Could not unload $m (in use; will be blocked after reboot)"
fi
done# Should print "/bin/false" — that's the new "install" command for esp4
sudo modprobe -n -v esp4
# Expected output: install /bin/false
# Should be empty or show only xfrm core (which is in-tree, not a separate module)
lsmod | grep -E 'esp|rxrpc|xfrm'Ubuntu/Debian: Update initramfs so the blacklist applies on every boot, including before /etc/modprobe.d is mounted from the rootfs (which matters if your rootfs is encrypted/networked).
sudo update-initramfs -uCachyOS / Arch: Update mkinitcpio for the same reason.
sudo mkinitcpio -PAlpine diskless mode: This is the critical one — without lbu commit -d, the blacklist is in RAM only and disappears on reboot.
# Check if you're in diskless mode
if grep -q "^tmpfs / tmpfs" /proc/mounts || [[ -f /etc/lbu/lbu.conf ]]; then
echo "Diskless detected, committing to LBU"
sudo lbu commit -d
fiVoid Linux: Mkinitcpio analogue is mkinitfs (musl) or dracut (newer glibc). Or just xbps-reconfigure:
sudo xbps-reconfigure -f $(xbps-query -s linux | head -1 | awk '{print $2}')This BREAKS:
- strongSwan, Libreswan, any kernel-mode IPsec (XFRM-based)
- IPsec VPNs configured via
ip xfrm - Anything that uses
xfrm_usernetlink - kAFS clients (you don't have these)
This does NOT break:
- WireGuard (uses
wireguard.ko, completely separate) - Tailscale (uses WireGuard under the hood)
- OpenVPN (TLS-based, userspace)
- SSH, HTTPS, anything else network-related
- 99.9% of normal desktop/server usage
If your machine is a CachyOS desktop running Tailscale on a Rust server, the mitigation breaks nothing. If your machine is a hardware firewall doing IPsec to a corporate office, do not apply this — patch instead.
Before building any kernel, two things matter: is the patch already in the source you'd be patching, and does the symbol the patch references actually exist in your kernel version?
The check is simple: extract the kernel source your distro would build from, and grep esp4.c for the new condition. Each distro section has the exact commands; here's what you're looking for:
# Generic, works once you have the source extracted somewhere
grep -A 4 "skb_has_frag_list(skb)" path/to/net/ipv4/esp4.c | head -20Patched (you're done):
} else if (!skb_has_frag_list(skb) &&
!skb_has_shared_frag(skb)) {
nfrags = skb_shinfo(skb)->nr_frags;Vulnerable (you need to patch):
} else if (!skb_has_frag_list(skb)) {
nfrags = skb_shinfo(skb)->nr_frags;The difference is one line. If you see skb_has_shared_frag anywhere in the conditional, the upstream fix is in. Our local hardening adds !skb->data_len on top — if you want that too, build the patched kernel anyway. If you trust upstream's tag-at-source approach, the official kernel is enough.
The patch references this constant. If your kernel doesn't have it, the build will fail. Check with:
grep -rn "MSG_NO_SHARED_FRAGS" path/to/include/If the grep returns hits: the patch applies as-is.
If the grep returns nothing: edit dirtyfrag-esp-fix.patch to drop the conditional. Find these two lines (they appear twice, once in each ip_output.c hunk):
+ if (!(flags & MSG_NO_SHARED_FRAGS))
+ skb_shinfo(skb)->flags |= SKBFL_SHARED_FRAG;
Change to (just delete the if line, dedent the second):
+ skb_shinfo(skb)->flags |= SKBFL_SHARED_FRAG;
Or do it with sed:
# Strips the conditional, always tags
sed -i '/^+\s*if (!(flags & MSG_NO_SHARED_FRAGS))$/d' dirtyfrag-esp-fix.patchThe functional difference: the conditional version skips tagging when called from internal kernel paths that pass MSG_NO_SHARED_FRAGS (because those callers know the pages are kernel-owned). The unconditional version always tags. Both are safe — the unconditional version just costs a tiny extra skb_cow_data() call on those internal paths, which is invisible on a desktop.
Knowing what you're working with prevents surprises.
# Architecture
uname -m # x86_64, aarch64, armv7l, etc.
# Kernel version
uname -r
# Is this a Raspberry Pi?
[[ -f /proc/device-tree/model ]] && cat /proc/device-tree/model | tr -d '\0'
# Output like: Raspberry Pi 5 Model B Rev 1.0
# Is this a virtual machine?
[[ -f /sys/class/dmi/id/sys_vendor ]] && cat /sys/class/dmi/id/sys_vendor
# Output like: QEMU, KVM, VMware, Xen — or your motherboard manufacturer
# Bootloader detection
if [[ -d /boot/loader/entries ]]; then echo "systemd-boot"
elif command -v limine-update &>/dev/null; then echo "limine"
elif [[ -f /boot/grub/grub.cfg ]] || [[ -f /boot/grub2/grub.cfg ]]; then echo "grub"
elif command -v update-extlinux &>/dev/null && [[ -f /etc/update-extlinux.conf ]]; then echo "extlinux"
elif [[ -f /boot/config.txt ]]; then echo "rpi-firmware"
else echo "unknown — check manually"
fi
# Secure Boot status
mokutil --sb-state 2>/dev/null
# Output: SecureBoot enabled / SecureBoot disabledThis is non-negotiable. Never reboot into a freshly-built custom kernel as your only kernel.
# Arch / CachyOS — install LTS as fallback
sudo pacman -S linux-cachyos-lts linux-cachyos-lts-headers
# Ubuntu — keep your current generic kernel installed; check it's in GRUB
ls /boot/vmlinuz-*
awk -F\' '/^menuentry |^submenu / {print $2}' /boot/grub/grub.cfg
# Alpine — install linux-virt or linux-edge as fallback
sudo apk add linux-virt # smaller, KVM-friendly fallback
# Void — install linux-lts as fallback
sudo xbps-install -Sy linux-lts linux-lts-headersAfter installing the fallback, reboot into it once, confirm it works, then come back and proceed.
CachyOS uses Arch's pacman and builds from PKGBUILD recipes via makepkg. The kernel repo at github.com/CachyOS/linux-cachyos has one directory per variant: linux-cachyos/ (default EEVDF), linux-cachyos-lts/ (LTS BORE), linux-cachyos-lto/ (Clang+ThinLTO+AutoFDO), and several others. As of May 2 2026, the default version is 7.0.3-1.1.
uname -r
# 7.0.3-1-cachyos → linux-cachyos (default, EEVDF)
# 7.0.3-1-cachyos-lto → linux-cachyos-lto (Clang+ThinLTO+AutoFDO)
# 6.18.26-1-cachyos-lts → linux-cachyos-lts (LTS BORE)
# *-cachyos-bore → linux-cachyos-bore
# *-cachyos-bmq → linux-cachyos-bmq
# *-cachyos-hardened → linux-cachyos-hardened
# *-cachyos-rc → linux-cachyos-rc
# *-cachyos-server → linux-cachyos-server
# *-cachyos-rt-bore → linux-cachyos-rt-bore (real-time)The variant matters because each has its own directory in the repo with its own PKGBUILD and config. You need to operate in the variant directory matching what you're running.
grep -E '^\[cachyos' /etc/pacman.conf
# Look for: cachyos-v3, cachyos-v4, or cachyos-znver4
# This determines which prebuilt NVIDIA package to pull latersudo pacman -S --needed \
base-devel bc cpio gettext kmod libelf pahole \
perl python tar xz zstd git mkinitcpio pacman-contrib
# Required for Secure Boot signing later (if SB enabled)
sudo pacman -S --needed sbctl
# Required for LTO variants (default linux-cachyos uses Clang since 7.0)
sudo pacman -S --needed clang llvm lld
# Optional speedups
sudo pacman -S --needed ccache pigz pixz# If you're already on linux-cachyos-lts, skip this
sudo pacman -S linux-cachyos-lts linux-cachyos-lts-headers
sudo mkinitcpio -P
# Detect bootloader and update accordingly
if [[ -d /boot/loader/entries ]]; then
sudo bootctl list | grep -i lts
elif command -v limine-update &>/dev/null; then
sudo limine-update
elif [[ -f /boot/grub/grub.cfg ]]; then
sudo grub-mkconfig -o /boot/grub/grub.cfg
fiReboot into linux-cachyos-lts now. Confirm it boots and your system works. Reboot back into your normal kernel before proceeding.
sudo bootctl status 2>/dev/null | grep -i 'secure boot'
mokutil --sb-state 2>/dev/nullIf Secure Boot is enabled, you have two options:
Option A: disable in firmware. Reboot into your firmware setup, find Secure Boot, set it to disabled, save and exit. Easiest path.
Option B: sign your custom kernel with sbctl. Keeps SB on. Requires one-time setup:
sudo sbctl status
# If "Setup Mode" is "Enabled", you can enroll your own keys:
sudo sbctl create-keys
sudo sbctl enroll-keys -m
# Reboot, confirm in firmware, come back. From now on, sbctl can sign kernels.cd ~
git clone --depth 1 https://github.com/CachyOS/linux-cachyos.git
cd linux-cachyos
# Navigate to YOUR variant — example for default:
cd linux-cachyos
# or for LTS:
# cd linux-cachyos-lts
# or for LTO:
# cd linux-cachyos-lto
ls
# Should see: PKGBUILD, config, auto-cpu-optimization.sh, possibly other files# This downloads + extracts but doesn't build (~2 min)
makepkg -o --skippgpcheck
# Find the extracted source
ls src/
# linux-7.0.3 (or similar)
cd src/linux-*/
grep -A 4 "skb_has_frag_list(skb)" net/ipv4/esp4.c | head -10
# If you see !skb_has_shared_frag, the upstream patch is in.
# If not, continue with patching.
cd ../.. # back to the variant directoryAlso check MSG_NO_SHARED_FRAGS:
grep -rn "MSG_NO_SHARED_FRAGS" src/linux-*/include/
# Empty output → edit the patch (see Section 5.2)cp ~/dirtyfrag-patches/dirtyfrag-esp-fix.patch .
# Only if you actually use kAFS (you don't):
# cp ~/dirtyfrag-patches/dirtyfrag-rxrpc-fix.patch .
ls *.patch
# dirtyfrag-esp-fix.patchIf MSG_NO_SHARED_FRAGS was missing, edit the patch:
sed -i '/^+\s*if (!(flags & MSG_NO_SHARED_FRAGS))$/d' dirtyfrag-esp-fix.patchYou need four edits. Here's the complete walkthrough.
nano PKGBUILD
# or: $EDITOR PKGBUILDEdit 1: Rename pkgbase so your kernel coexists with the official one. This means pacman -Syu won't clobber your build, and you can pick at boot time.
Find:
pkgbase=linux-cachyosChange to:
pkgbase=linux-cachyos-dirtyfrag
provides=(linux-cachyos)(For LTS variant, the original line is pkgbase=linux-cachyos-lts — change to pkgbase=linux-cachyos-lts-dirtyfrag and provides=(linux-cachyos-lts).)
Edit 2: Add patches to the source=() array. Find the source array — it's a multi-line bash array starting with source=(. Add your patch filename inside, before the closing ). The exact location doesn't matter (Arch convention: at the end), but it must be inside the parens.
Example before:
source=(
"https://cdn.kernel.org/pub/linux/kernel/v7.x/linux-${_major}.tar.xz"
"https://github.com/CachyOS/kernel-patches/archive/refs/heads/master.tar.gz"
"config"
"auto-cpu-optimization.sh"
)Example after:
source=(
"https://cdn.kernel.org/pub/linux/kernel/v7.x/linux-${_major}.tar.xz"
"https://github.com/CachyOS/kernel-patches/archive/refs/heads/master.tar.gz"
"config"
"auto-cpu-optimization.sh"
"dirtyfrag-esp-fix.patch"
)Edit 3: Add patch invocation to prepare(). Find the prepare() function. It will look something like:
prepare() {
cd "linux-${_major}"
echo "Setting version..."
echo "-$pkgrel" > localversion.10-pkgrel
echo "${pkgbase#linux}" > localversion.20-pkgname
# Apply CachyOS patches
local src
for src in "${source[@]}"; do
src="${src%%::*}"
src="${src##*/}"
src="${src%.zst}"
[[ $src = *.patch ]] || continue
echo "Applying patch $src..."
patch -Np1 < "../$src"
done
# ... config stuff ...
}If the existing loop already auto-applies all *.patch files from source=(), then adding to source is enough — no manual patch line needed. That's the case in current CachyOS PKGBUILDs. But to be safe, add an explicit invocation just before # config stuff:
# === DIRTY FRAG SECURITY PATCH ===
if ! grep -q "skb_has_shared_frag" net/ipv4/esp4.c; then
echo "Applying Dirty Frag ESP fix (CVE-2026-43284)..."
patch -Np1 --fuzz=5 < "$srcdir/dirtyfrag-esp-fix.patch" || {
echo "ERROR: Dirty Frag patch failed — apply manually" >&2
return 1
}
else
echo "Dirty Frag ESP fix already in source, skipping local patch"
fi
# === END DIRTY FRAG ===The if ! grep -q guard means if upstream's already in (or the CachyOS auto-loop already applied it), we don't double-apply.
Edit 4: Update checksums. Without this, makepkg refuses to start.
# Save your edits to PKGBUILD, exit the editor, then:
updpkgsumsupdpkgsums is from pacman-contrib. It rewrites the b2sums=() (or sha256sums=()) line with checksums for your new source files. If updpkgsums chokes (it sometimes does on git+... sources), regenerate manually:
makepkg -g >> PKGBUILD
# Then manually delete the OLD checksum block, keeping only the new one at the bottom# Check your RAM situation — LTO can OOM on 16GB
free -h
# If you have 16GB or less and your variant is LTO:
MAKEFLAGS="-j$(($(nproc) / 2))" makepkg -s --skippgpcheck 2>&1 | tee build.log
# If you have 32GB+:
MAKEFLAGS="-j$(nproc)" makepkg -s --skippgpcheck 2>&1 | tee build.log
# With ccache for faster rebuilds:
export PATH="/usr/lib/ccache/bin:$PATH"
MAKEFLAGS="-j$(nproc)" makepkg -s --skippgpcheck 2>&1 | tee build.logThe build will take 25-90 minutes depending on CPU and whether LTO is enabled. Watch for:
==> Applying Dirty Frag ESP fix (CVE-2026-43284)...
patching file net/ipv4/esp4.c
Hunk #1 succeeded at 875 with fuzz 2 (offset 2 lines).
patching file net/ipv4/ip_output.c
Hunk #1 succeeded at 1239 (offset 6 lines).
patching file net/ipv6/esp6.c
Hunk #1 succeeded at 917 with fuzz 2 (offset 2 lines).
patching file net/ipv6/ip6_output.c
Hunk #1 succeeded at 1798 (offset 4 lines).
This is what success looks like — fuzz and offset are normal because the patch is from kernel ~6.13 and you're applying to ~7.0.
If you see Hunk #N FAILED, the line numbers shifted too far for --fuzz=5 to find them. See Troubleshooting Section 15.1.
After the build completes:
ls -la *.pkg.tar.zst
# linux-cachyos-dirtyfrag-7.0.3-1-x86_64.pkg.tar.zst
# linux-cachyos-dirtyfrag-headers-7.0.3-1-x86_64.pkg.tar.zstsudo pacman -U linux-cachyos-dirtyfrag-*.pkg.tar.zstThis is critical if you have NVIDIA, ZFS, VirtualBox, or any other DKMS-managed module. Without this step, you reboot to a blank screen.
sudo dkms autoinstall
sudo dkms statusFor NVIDIA specifically:
# What's installed?
pacman -Qs nvidia
# 5060 Ti (Blackwell) REQUIRES the open kernel modules:
sudo pacman -S nvidia-open-dkms
sudo dkms autoinstall
# Or, if CachyOS built nvidia-open as part of your kernel build:
sudo pacman -S linux-cachyos-dirtyfrag-nvidia-open
# 3090 (Ampere) — either variant works:
sudo pacman -S nvidia-dkms # closed-source (legacy support)
# OR
sudo pacman -S nvidia-open-dkms # open kernel modules (recommended)The pacman post-install hook should do this automatically, but verify:
sudo mkinitcpio -P
ls -la /boot/initramfs-linux-cachyos-dirtyfrag*.img
# Both fallback and main should have current timestampsif mokutil --sb-state 2>/dev/null | grep -q "enabled"; then
sudo sbctl sign -s /boot/vmlinuz-linux-cachyos-dirtyfrag
fi# systemd-boot
if [[ -d /boot/loader/entries ]]; then
sudo bootctl update
fi
# limine
if command -v limine-update &>/dev/null; then
sudo limine-update
fi
# grub
if [[ -f /boot/grub/grub.cfg ]]; then
sudo grub-mkconfig -o /boot/grub/grub.cfg
fiIf Secure Boot is OFF, you can use kexec for a fast test cycle:
sudo kexec -l /boot/vmlinuz-linux-cachyos-dirtyfrag \
--initrd=/boot/initramfs-linux-cachyos-dirtyfrag.img \
--reuse-cmdline
sudo systemctl kexecIf Secure Boot is ON, just reboot — kexec under SB is unreliable (lockdown blocks kexec_load, kexec_file_load requires MOK enrollment of the key sbctl signed with):
sudo rebootIf the boot hangs, power-cycle and pick linux-cachyos-lts from the boot menu.
See Section 13 for cross-distro verification. Quick check:
uname -r
# Should show: 7.0.3-1-cachyos-dirtyfrag (or your variant)
# DKMS sanity
sudo dkms status
# NVIDIA sanity (if applicable)
nvidia-smiThis is what the procedure actually looked like on a CachyOS box (kernel 7.0.3, 5060 Ti + 3090, 32GB RAM, Secure Boot disabled, limine bootloader):
# t=0min: pre-flight
uname -r # 7.0.3-1-cachyos
free -h # 32GB available
mokutil --sb-state # SecureBoot disabled
# t=2min: deps + fallback
sudo pacman -S --needed --noconfirm \
base-devel bc cpio gettext kmod libelf pahole \
perl python tar xz zstd git mkinitcpio pacman-contrib clang llvm lld
sudo pacman -S linux-cachyos-lts linux-cachyos-lts-headers
sudo mkinitcpio -P
sudo limine-update
# (reboot into LTS, confirm works, reboot back)
# t=15min: clone + check
cd ~
git clone --depth 1 https://github.com/CachyOS/linux-cachyos.git
cd linux-cachyos/linux-cachyos
makepkg -o --skippgpcheck # ~2 min download+extract
grep -A 4 "skb_has_frag_list(skb)" src/linux-*/net/ipv4/esp4.c | head
# Saw: !skb_has_shared_frag(skb) — upstream patch already in!
# DECISION: skip custom build, go to verify
# t=20min: verify on running stock kernel
sudo modprobe -n -v esp4 # /bin/false (blacklist active from earlier)
# Switch to a clean state for verify by booting unblocked
sudo rm /etc/modprobe.d/dirtyfrag-mitigation.conf
sudo reboot
# After reboot:
sudo bpftrace -e 'kfunc:vmlinux:skb_cow_data { @[comm] = count(); }' &
sleep 30
# Generate any IPsec-ish traffic if configured, or just wait
sudo killall bpftrace
# Confirmed esp_input is taking the cow path on splice frags
# Total time: 25 min, no kernel build neededThis is the typical outcome on May 2026+ CachyOS — the upstream patch is already in, you only need verify.
If you did build a custom kernel and CachyOS later ships an official patched build:
sudo pacman -Syu
sudo pacman -R linux-cachyos-dirtyfrag linux-cachyos-dirtyfrag-headers
# (or whichever variant you renamed)
sudo limine-update # or your bootloader
sudo rebootUbuntu 26.04 LTS (Resolute Raccoon) ships kernel 7.0 by default, released April 23 2026. Three paths to a patched kernel: wait for SRU, mainline PPA, or full source build. For most users, wait for SRU.
Chen's patch has Cc: stable@vger.kernel.org. Canonical's kernel team picks up stable backports through the Stable Release Update process, typically within 1-2 weeks. To use this path:
# Apply Phase 0 mitigation
sudo tee /etc/modprobe.d/dirtyfrag-mitigation.conf > /dev/null << 'EOF'
install esp4 /bin/false
install esp6 /bin/false
install rxrpc /bin/false
install ipcomp4 /bin/false
install ipcomp6 /bin/false
install xfrm_user /bin/false
EOF
sudo update-initramfs -u
# Set up unattended security upgrades (so the SRU lands automatically)
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
# Watch for the kernel upgrade
sudo apt update
apt list --upgradable | grep linux
# When it appears, install:
sudo apt upgrade -y
sudo reboot
# Verify (Section 13)That's the entire procedure. Most readers will be done here.
Canonical's kernel team maintains pre-built .deb packages of mainline kernels at kernel.ubuntu.com. These are vanilla upstream — no Canonical config tweaks, no AppArmor profiles tuned for Ubuntu, no Pro Livepatch support. They are also unsigned for Ubuntu's MOK, so they will NOT boot under Secure Boot unless you disable SB.
# Add the cappelikan PPA (community-maintained tool that wraps kernel.ubuntu.com)
sudo add-apt-repository -y ppa:cappelikan/ppa
sudo apt update
sudo apt install -y mainline
# CLI usage
mainline list-installed
mainline list # see all available versions
mainline install-latest # get newest stable
# Or pick a specific version
mainline install v7.0.4
# Update GRUB
sudo update-grub
# Reboot
sudo rebootManual download (no PPA):
TARGET="v7.0.4"
cd /tmp
for f in \
linux-headers-7.0.4-070004_*_all.deb \
linux-headers-7.0.4-070004-generic_*_amd64.deb \
linux-image-unsigned-7.0.4-070004-generic_*_amd64.deb \
linux-modules-7.0.4-070004-generic_*_amd64.deb; do
wget -c "https://kernel.ubuntu.com/mainline/${TARGET}/amd64/${f}"
done
sudo dpkg -i linux-*.deb
sudo update-grub
sudo rebootThe cleanest path if you need a Canonical-flavored kernel with your patch. Uses Launchpad source, applies via debian/rules, produces proper .deb packages.
Ubuntu 26.04 uses the new deb822 .sources format:
# Modern format (26.04)
if [[ -f /etc/apt/sources.list.d/ubuntu.sources ]]; then
sudo sed -i 's/^Types: deb$/Types: deb deb-src/' /etc/apt/sources.list.d/ubuntu.sources
fi
# Old format (24.04 and earlier)
if [[ -f /etc/apt/sources.list ]]; then
sudo sed -i '/deb-src/s/^# //' /etc/apt/sources.list
fi
sudo apt updatesudo apt install -y \
fakeroot dpkg-dev libelf-dev libssl-dev \
libncurses-dev bison flex bc rsync zstd dwarves \
git
sudo apt build-dep -y linux linux-image-unsigned-$(uname -r)The Launchpad URL slug for 26.04 is resolute, not 26.04. (For 24.04 it's noble. For 24.10 it's oracular.)
mkdir -p ~/ubuntu-kernel
cd ~/ubuntu-kernel
# 26.04
git clone --depth 1 \
git://git.launchpad.net/~ubuntu-kernel/ubuntu/+source/linux/+git/resolute \
-b master-next
cd resoluteIf master-next doesn't exist, try master:
git clone --depth 1 \
git://git.launchpad.net/~ubuntu-kernel/ubuntu/+source/linux/+git/resolutegrep -A 4 "skb_has_frag_list(skb)" net/ipv4/esp4.c | head
# Already patched? Skip the rebuild.
grep -rn "MSG_NO_SHARED_FRAGS" include/
# Empty? Edit the patch (Section 5.2).patch -p1 --fuzz=5 < ~/dirtyfrag-patches/dirtyfrag-esp-fix.patchExpected output:
patching file net/ipv4/esp4.c
Hunk #1 succeeded at 875 with fuzz 2 (offset 2 lines).
patching file net/ipv4/ip_output.c
Hunk #1 succeeded at 1241 (offset 8 lines).
patching file net/ipv6/esp6.c
Hunk #1 succeeded at 919 with fuzz 2 (offset 4 lines).
patching file net/ipv6/ip6_output.c
Hunk #1 succeeded at 1802 (offset 8 lines).
Do NOT use CONFIG_LOCALVERSION — it breaks debian/rules. Use the changelog suffix instead:
# Bumps the version to e.g., 7.0.0-12.13+dirtyfrag1
sed -i '0,/^linux (/{s/^linux (\([^)]*\))/linux (\1+dirtyfrag1)/}' \
debian.master/changelog
# Verify
head -1 debian.master/changelog
# Expected: linux (7.0.0-12.13+dirtyfrag1) resolute; urgency=mediumchmod +x debian/rules debian/scripts/* debian/scripts/misc/*
fakeroot debian/rules clean
# Disable trusted/revocation key requirements (will fail without these)
scripts/config --set-str SYSTEM_TRUSTED_KEYS ""
scripts/config --set-str SYSTEM_REVOCATION_KEYS ""# Adjust -j based on your RAM (LTO can use 4-8GB per linker invocation)
free -h
# Aggressive
LANG=C fakeroot debian/rules binary-headers binary-generic binary-perarch \
skipabi=true skipmodule=true skipretpoline=true \
-j$(nproc) 2>&1 | tee build.log
# Conservative (16GB RAM or less)
LANG=C fakeroot debian/rules binary-headers binary-generic binary-perarch \
skipabi=true skipmodule=true skipretpoline=true \
-j$(($(nproc) / 2)) 2>&1 | tee build.logThe skip flags are essential for non-official builds:
skipabi=true— don't enforce ABI compatibility checksskipmodule=true— don't enforce module checksskipretpoline=true— don't generate retpoline data
Build time: 30-90 minutes. Output .deb files appear in the parent directory.
cd ..
ls *.deb
# linux-headers-7.0.0-12_*+dirtyfrag1*_all.deb
# linux-headers-7.0.0-12-generic_*+dirtyfrag1*_amd64.deb
# linux-image-unsigned-7.0.0-12-generic_*+dirtyfrag1*_amd64.deb
# linux-modules-7.0.0-12-generic_*+dirtyfrag1*_amd64.deb
sudo dpkg -i \
linux-headers-*+dirtyfrag1*.deb \
linux-image-unsigned-*+dirtyfrag1*.deb \
linux-modules-*+dirtyfrag1*.deb# What DKMS modules are present?
sudo dkms status
# NVIDIA specifically
nv_pkg=$(dpkg -l | awk '/nvidia-dkms-/{print $2}' | head -1)
sudo dpkg-reconfigure "$nv_pkg"
# Or for ZFS
sudo apt install --reinstall zfs-dkms
# Or for any other DKMS module
sudo dkms autoinstallsudo update-grub
# Restart AppArmor — its profiles are loaded per-kernel
sudo systemctl restart apparmorUbuntu's source build doesn't automatically sign with arbitrary MOKs. If you have SB enabled and need it signed:
# Install sbsigntools and sbctl-like tooling
sudo apt install sbsigntool
# If you have your own MOK key/cert:
sudo sbsign --key /path/to/MOK.priv --cert /path/to/MOK.pem \
--output /boot/vmlinuz-7.0.0-12-generic+dirtyfrag1 \
/boot/vmlinuz-7.0.0-12-generic+dirtyfrag1
# If you don't, see Ubuntu's MOK enrollment guidesudo rebootGRUB defaults to the highest-versioned kernel, which is your +dirtyfrag1 build. If anything goes wrong, pick the previous kernel from GRUB's "Advanced options for Ubuntu" submenu.
- AppArmor profiles are loaded per-kernel. Custom kernel may need
sudo systemctl restart apparmorif profiles get confused on first boot. - Snap confinement uses kernel features (apparmor, seccomp, cgroup hierarchies). If your build differs from Canonical's config in those areas, snaps may fail to start.
- cgroup v1 is gone in 26.04. Don't disable any
CONFIG_CGROUP_*options. - Mainline + Secure Boot = no boot. If SB is on, use Path C with sbsign.
- The new boot partition layout in 26.04 does A/B boot testing on Pi installs. Custom kernels need to play nicely with
piboot-try. - GCC 15.2 is default in 26.04. Kernel 7.0 builds fine with it; older custom kernels may need older GCC via
update-alternatives.
# Check what's available
sudo apt update
apt list --upgradable | grep linux
# Install official patched kernel
sudo apt upgrade -y
# Remove your custom kernel
sudo apt remove --purge linux-image-unsigned-*+dirtyfrag1* \
linux-headers-*+dirtyfrag1* linux-modules-*+dirtyfrag1*
# Or if mainline:
sudo mainline uninstall v7.0.4 # whichever you installed
sudo update-grub
# Once verified the official kernel has the patch:
sudo rm /etc/modprobe.d/dirtyfrag-mitigation.conf
sudo update-initramfs -u
sudo rebootAlpine is the simplest of the three glibc/musl-mixed distros for this task: small kernel config, fast build, no NVIDIA proprietary mess, and rxrpc.ko isn't even in the default config so the RxRPC patch is irrelevant.
cat /etc/alpine-release # 3.23.x
uname -r # e.g., 6.18.26-0-lts
uname -m # x86_64
# Confirm rxrpc isn't built (saves you the patch)
modprobe --dry-run rxrpc 2>&1
# Expected: modprobe: FATAL: Module rxrpc not found...
# Confirm ESP IS built
modprobe --dry-run esp4 2>&1
modprobe --dry-run esp6 2>&1
# Both should resolve to .ko pathsIf you only use WireGuard/Tailscale on this Alpine box, the Phase 0 mitigation is your complete fix. Stop reading this section.
sudo apk add alpine-sdk
sudo addgroup $(whoami) abuild
# Log out and back in, or:
newgrp abuild
# Generate signing key (one-time)
abuild-keygen -a -n
sudo cp ~/.abuild/*.rsa.pub /etc/apk/keys/
sudo chmod 644 /etc/apk/keys/*.rsa.pubsudo apk add \
alpine-sdk build-base bc bison flex \
elfutils-dev openssl-dev linux-headers \
perl python3 diffutils findutils \
xz zstd tar git sed coreutilsgit clone --depth 1 --branch 3.23-stable \
https://gitlab.alpinelinux.org/alpine/aports.git ~/aports
cd ~/aports/main/linux-lts
ls
# APKBUILD, config-lts.x86_64, config-lts.aarch64, etc.The branch matters. Use 3.23-stable for kernel 6.18, edge for bleeding edge. Your running kernel determines which is right — check with uname -r.
abuild fetch
abuild unpack
# Find extracted source
ls src/
# linux-6.18.26 (or whatever)
cd src/linux-*/
grep -A 4 "skb_has_frag_list(skb)" net/ipv4/esp4.c | head
# Patched? Skip rebuild.
grep -rn "MSG_NO_SHARED_FRAGS" include/
# Empty? Edit patch.
cd ../.. # back to aports/main/linux-ltscp ~/dirtyfrag-patches/dirtyfrag-esp-fix.patch .
# If MSG_NO_SHARED_FRAGS was missing
# sed -i '/^+\s*if (!(flags & MSG_NO_SHARED_FRAGS))$/d' dirtyfrag-esp-fix.patchThree changes. Open with your editor:
nano APKBUILD
# or: vi APKBUILDChange 1: Use a custom flavor. This makes your kernel install side-by-side with the stock one. Find:
_flavor="lts"Or, if no _flavor exists, the variable might be inferred from pkgname. Add at the top of the file:
_flavor="lts-dirtyfrag"If _flavor already exists, change its value to "lts-dirtyfrag".
Change 2: Add patch to source. Find the source= block (it's typically a multi-line variable):
source="https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-$_kernver.tar.xz
config-lts.x86_64
config-lts.aarch64
"Add your patch:
source="https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-$_kernver.tar.xz
config-lts.x86_64
config-lts.aarch64
dirtyfrag-esp-fix.patch
"Change 3: Inject patch application into prepare(). Find the prepare() function. It often calls default_prepare:
prepare() {
default_prepare
# ... existing stuff ...
}Add the patch line after default_prepare:
prepare() {
default_prepare
# === DIRTY FRAG SECURITY PATCH ===
if ! grep -q "skb_has_shared_frag" "$builddir/net/ipv4/esp4.c"; then
msg "Applying Dirty Frag ESP fix (CVE-2026-43284)..."
patch -p1 --fuzz=5 -d "$builddir" \
< "$srcdir/dirtyfrag-esp-fix.patch" \
|| die "Dirty Frag patch failed"
else
msg "Dirty Frag fix already in source, skipping"
fi
# === END DIRTY FRAG ===
}If prepare() doesn't exist at all, add the entire function near the top of the file, after the variable declarations.
abuild checksumThis regenerates the sha512sums= line at the bottom of the APKBUILD with your new patch's checksum included.
abuild -r 2>&1 | tee build.logThe -r flag installs missing dependencies automatically. Build time is typically 10-20 minutes on modern hardware (Alpine's kernel config is much smaller than CachyOS or Ubuntu).
Expected output during patch step:
>>> linux-lts-dirtyfrag: Applying Dirty Frag ESP fix (CVE-2026-43284)...
patching file net/ipv4/esp4.c
Hunk #1 succeeded at 875 with fuzz 2 (offset 2 lines).
patching file net/ipv4/ip_output.c
patching file net/ipv6/esp6.c
patching file net/ipv6/ip6_output.c
After the build:
ls ~/packages/main/x86_64/linux-lts-dirtyfrag*.apk
# linux-lts-dirtyfrag-6.18.26-r0.apk
# linux-lts-dirtyfrag-dev-6.18.26-r0.apk
# linux-lts-dirtyfrag-doc-6.18.26-r0.apksudo apk add --allow-untrusted ~/packages/main/x86_64/linux-lts-dirtyfrag*.apk# extlinux is the default on most Alpine x86_64 installs
if command -v update-extlinux &>/dev/null; then
sudo update-extlinux
fi
# grub fallback
if [[ -f /boot/grub/grub.cfg ]]; then
sudo grub-mkconfig -o /boot/grub/grub.cfg
fi
# syslinux variant
if [[ -f /etc/update-extlinux.conf ]]; then
sudo update-extlinux
fiVerify your new kernel appears in the boot menu:
ls /boot/
# vmlinuz-lts (stock)
# vmlinuz-lts-dirtyfrag (yours)
# initramfs-lts.img
# initramfs-lts-dirtyfrag.img
# extlinux.conf
cat /boot/extlinux.conf | grep LABEL -A 4This is the Alpine-specific gotcha that bites everyone the first time. If you boot from RAM disc (LBU mode), nothing in /etc or /boot is persistent unless you commit:
if grep -q "^tmpfs / tmpfs" /proc/mounts || [[ -f /etc/lbu/lbu.conf ]]; then
echo "Diskless mode detected — committing"
sudo lbu commit -d
fiDo this BEFORE rebooting. If you skip it, the next boot loads the stock kernel from your USB and your custom kernel disappears.
sudo reboot
# After reboot:
uname -r
# Expected: 6.18.26-0-lts-dirtyfrag
# Verify
sudo apk verify linux-lts-dirtyfragSee Section 13 for cross-distro verification.
# When Alpine ships the official patched kernel
sudo apk update
sudo apk upgrade
# Switch back to stock
sudo apk del linux-lts-dirtyfrag linux-lts-dirtyfrag-dev linux-lts-dirtyfrag-doc
sudo apk add linux-lts linux-lts-dev linux-lts-doc
# Bootloader
sudo update-extlinux
# Diskless persist
[[ -f /etc/lbu/lbu.conf ]] && sudo lbu commit -d
sudo rebootThe aarch64 path is mostly the same as x86_64 but with three significant differences: the kernel package is linux-rpi (not linux-lts), the bootloader is the Pi firmware (not extlinux), and you might be cross-compiling from x86 instead of building on the Pi itself.
uname -m
# aarch64
cat /proc/device-tree/model | tr -d '\0'
# Raspberry Pi 5 Model B Rev 1.0
# (or "Raspberry Pi 4 Model B Rev 1.5", etc.)If you're on aarch64 in a VM (KVM guest, AWS Graviton, etc.), use linux-virt or linux-lts instead of linux-rpi — same as Phase 3 but with -r aarch64 arch.
Build on the Pi (simplest): A Pi 5 with 8GB RAM builds the linux-rpi kernel in ~30-60 minutes. A Pi 4 takes ~60-90 minutes. Just follow this section's commands directly on the Pi.
Cross-compile from x86_64 (faster): Set up an Alpine x86_64 build host with QEMU binfmt for aarch64. Saves ~70% wall-clock time but adds setup complexity.
# On x86_64 Alpine host
sudo apk add qemu-aarch64 qemu-openrc binfmt-support
sudo rc-service qemu-binfmt start
sudo rc-update add qemu-binfmt default
# Verify binfmt registration
ls /proc/sys/fs/binfmt_misc/
# Expected: qemu-aarch64 entry
# When running abuild later, target aarch64:
abuild -r -A aarch64For this guide, I'll assume you're building on the Pi.
Same as x86_64:
sudo apk add \
alpine-sdk build-base bc bison flex \
elfutils-dev openssl-dev linux-headers \
perl python3 diffutils findutils \
xz zstd tar git sed coreutils
sudo addgroup $(whoami) abuild
abuild-keygen -a -n
sudo cp ~/.abuild/*.rsa.pub /etc/apk/keys/
sudo chmod 644 /etc/apk/keys/*.rsa.pubgit clone --depth 1 --branch 3.23-stable \
https://gitlab.alpinelinux.org/alpine/aports.git ~/aports
cd ~/aports/main/linux-rpi
ls
# APKBUILD, config-rpi.aarch64, possibly config-rpi.armv7abuild fetch
abuild unpack
ls src/
# linux-6.18.26-rpi (or similar)
cd src/linux-*/
grep -A 4 "skb_has_frag_list(skb)" net/ipv4/esp4.c | head
grep -rn "MSG_NO_SHARED_FRAGS" include/
cd ../..cp ~/dirtyfrag-patches/dirtyfrag-esp-fix.patch .
nano APKBUILDThe edits are identical to x86_64 (Section 8.7), but with _flavor="rpi-dirtyfrag":
_flavor="rpi-dirtyfrag"
# ... source= line gets dirtyfrag-esp-fix.patch ...
prepare() {
default_prepare
if ! grep -q "skb_has_shared_frag" "$builddir/net/ipv4/esp4.c"; then
msg "Applying Dirty Frag ESP fix (CVE-2026-43284)..."
patch -p1 --fuzz=5 -d "$builddir" \
< "$srcdir/dirtyfrag-esp-fix.patch" \
|| die "Dirty Frag patch failed"
fi
}abuild checksum
abuild -r 2>&1 | tee build.logBuild time on a Pi 5 with 8GB RAM and SD card storage: ~45 minutes. With NVMe storage on a Pi 5: ~30 minutes. With cross-compile from x86: ~10 minutes.
Output:
ls ~/packages/main/aarch64/linux-rpi-dirtyfrag*.apksudo apk add --allow-untrusted ~/packages/main/aarch64/linux-rpi-dirtyfrag*.apkThe Pi firmware ignores your bootloader entirely. It reads /boot/config.txt at boot time and loads whatever kernel image is named there. After installing your new kernel, you must update this file.
# Find the new kernel image
ls /boot/
# vmlinuz-rpi (stock)
# vmlinuz-rpi-dirtyfrag (yours)
# initramfs-rpi
# initramfs-rpi-dirtyfrag
# config.txt
# cmdline.txt
# bootcode.bin
# kernel8.img (stock 64-bit kernel — actually a copy/symlink)
# View current config.txt
cat /boot/config.txtYou'll typically see something like:
[all]
arm_64bit=1
kernel=vmlinuz-rpi
initramfs initramfs-rpi
include usercfg.txt
Change kernel= to your new kernel and add a matching initramfs line:
sudo nano /boot/config.txtModify to:
[all]
arm_64bit=1
kernel=vmlinuz-rpi-dirtyfrag
initramfs initramfs-rpi-dirtyfrag
include usercfg.txt
If you want to keep the option to boot stock, you can use Pi's conditional sections:
[all]
arm_64bit=1
[default]
kernel=vmlinuz-rpi
initramfs initramfs-rpi
[dirtyfrag]
kernel=vmlinuz-rpi-dirtyfrag
initramfs initramfs-rpi-dirtyfrag
include usercfg.txt
Then activate the [dirtyfrag] section by adding to /boot/cmdline.txt (or by setting os_prefix= in config.txt).
The Pi's kernel command line is in /boot/cmdline.txt. Don't change it unless you have a reason — your custom kernel still wants the same root device, etc.:
cat /boot/cmdline.txt
# Example:
# console=tty1 root=PARTUUID=xxxx-xx rootfstype=ext4 fsck.repair=yes rootwaitsudo rebootIf the Pi doesn't boot:
- Plug the SD card into another computer
- Edit
/boot/config.txtto revertkernel=back tovmlinuz-rpi - Reinsert and boot
- Diagnose
/boot is FAT32. Case-sensitivity issues with kernel filenames. Don't use camelCase or spaces.
rpi-update is dangerous. It pulls bleeding-edge firmware that may break compatibility with your custom kernel. Avoid it after you've built a custom kernel.
No extlinux on RPi. The Pi firmware reads config.txt directly. Don't run update-extlinux on a Pi — there's nothing for it to update.
GPU memory split is set in config.txt via gpu_mem=. Don't lose your VC4 acceleration after a kernel rebuild.
32-bit ARM is dropped in Alpine 3.23. RPi 2/3/Zero W on Alpine 3.23 is 64-bit only. RPi 1 and Zero (ARMv6) are not supported.
Pi 5 vs Pi 4. Pi 5 (BCM2712) and Pi 4 (BCM2711) have different kernel module sets. Both are in linux-rpi config but the right one is selected at runtime via device tree.
SD card wear. Building kernels on an SD card writes a lot. Use NVMe-attached storage on Pi 5 if possible, or build in tmpfs:
# Build in RAM (Pi 5 8GB only, kernel build needs ~6GB)
sudo mount -t tmpfs -o size=7G tmpfs /tmp/build
cd /tmp/build
git clone --depth 1 --branch 3.23-stable \
https://gitlab.alpinelinux.org/alpine/aports.git
cd aports/main/linux-rpi
# ... rest of procedureVoid Linux uses xbps for package management and xbps-src for building from void-packages. Init is runit, not systemd. Out-of-tree modules (NVIDIA, ZFS) are packaged as separate xbps packages, not DKMS — this changes how kernel updates flow.
cat /etc/os-release
# NAME="Void"
# ID=void
xbps-query -p version libc.so.6 2>/dev/null
# Should show glibc version (e.g., 2.42)
# musl Void shows musl-1.2.5 instead — see Phase 6 for thatUnlike Arch/Alpine which use generic names like linux-lts, Void uses per-version packages:
xbps-query -l | grep '^ii linux'
# ii linux6.18-6.18.26_1
# ii linux6.18-headers-6.18.26_1
# ii linux-headers-6.18.26_1
# ii linux-lts-6.18.26_1 <-- meta-packageYou'll be modifying the template at srcpkgs/linux6.18/template (or whichever version matches). For 7.0: srcpkgs/linux7.0/template.
sudo xbps-install -Sy \
base-devel bc cpio kmod libelf-devel pahole \
perl python3 tar xz zstd git# Clone void-packages
cd ~
git clone --depth 1 https://github.com/void-linux/void-packages.git
cd void-packages
# Bootstrap the build environment (this takes a few minutes)
./xbps-src binary-bootstrap# Trigger source download but don't build
./xbps-src fetch linux6.18
# Find the source
ls hostdir/sources/linux-6.18.26/
# Should have linux-6.18.26.tar.xz and possibly extracted dir
# Extract for inspection
cd hostdir/sources/linux-6.18.26/
tar xf linux-6.18.26.tar.xz
cd linux-6.18.26
grep -A 4 "skb_has_frag_list(skb)" net/ipv4/esp4.c | head
grep -rn "MSG_NO_SHARED_FRAGS" include/
cd ~/void-packagesVoid patches go in srcpkgs/<pkg>/patches/, applied in alphabetical order. Use a high prefix to ensure ours runs last.
mkdir -p srcpkgs/linux6.18/patches
cp ~/dirtyfrag-patches/dirtyfrag-esp-fix.patch \
srcpkgs/linux6.18/patches/9999-dirtyfrag-esp-fix.patch
# If MSG_NO_SHARED_FRAGS was missing
# sed -i '/^+\s*if (!(flags & MSG_NO_SHARED_FRAGS))$/d' \
# srcpkgs/linux6.18/patches/9999-dirtyfrag-esp-fix.patchxbps-src auto-applies patches in patches/, so you don't need to edit the template to invoke patch. But you do need to bump the revision number so xbps treats your build as newer.
nano srcpkgs/linux6.18/templateFind:
revision=1
Change to:
revision=99
(High revision = clearly your custom build. When Void ships the official fix, it'll be revision 2 or 3, and pacman/xbps will see your 99 as newer for now.)
Optionally, change the package name to coexist with the stock kernel:
pkgname=linux6.18-dirtyfrag
This requires also updating the replaces= and provides= fields:
# At the top of the template, after pkgname=:
replaces="linux6.18>=0"
provides="linux6.18-${version}_${revision}"# This adds the patch's checksum to the template
./xbps-src update-check linux6.18
# Or manually:
sha256sum srcpkgs/linux6.18/patches/9999-dirtyfrag-esp-fix.patch
# Add to checksum= block at bottom of template if needed./xbps-src pkg linux6.18 2>&1 | tee build.logxbps-src builds in a chroot at masterdir/, so it doesn't pollute your host. Build time: 20-40 minutes on modern hardware.
Watch for:
=> Applying patches/9999-dirtyfrag-esp-fix.patch
patching file net/ipv4/esp4.c
Hunk #1 succeeded at 875 with fuzz 2.
patching file net/ipv4/ip_output.c
patching file net/ipv6/esp6.c
patching file net/ipv6/ip6_output.c
After build:
ls hostdir/binpkgs/
# linux6.18-6.18.26_99.x86_64.xbps
# linux6.18-headers-6.18.26_99.x86_64.xbps# Add the local binpkgs as a repo
sudo xbps-install --repository=$(pwd)/hostdir/binpkgs \
-S linux6.18 linux6.18-headersVoid doesn't use DKMS the way Arch/Ubuntu do. Out-of-tree modules are packaged per-kernel-version:
# NVIDIA
sudo xbps-install -Sy nvidia
# (this rebuilds against the kernel headers you just installed)
# ZFS
sudo xbps-install -Sy zfsVoid uses xbps hooks to update the bootloader on kernel install. Verify they ran:
ls -la /boot/
# vmlinuz-6.18.26_1 (stock)
# vmlinuz-6.18.26_99 (yours, if same pkgname)
# initramfs-6.18.26_99.img
# GRUB
sudo grub-mkconfig -o /boot/grub/grub.cfg
# OR efibootmgr-based EFI boot
sudo efibootmgr -v
# OR syslinux
sudo /usr/libexec/xbps-kernel-hook.sh post-install linux6.18 6.18.26_99sudo reboot
uname -r
# Expected: 6.18.26_99 (or your variant)
xbps-query linux6.18
# Should show your custom revisionNo DKMS by default. When kernel rebuilds, you must also reinstall NVIDIA/ZFS as separate xbps packages. They don't auto-rebuild like DKMS would.
Per-version package names. linux6.18, linux6.19, linux7.0 are entirely separate packages. Switching kernel versions means installing a new package, not just upgrading.
runit boot order. After kernel install, Void uses xbps-trigger-update-grub (and others) to refresh the bootloader. If something goes wrong: sudo xbps-reconfigure -f linux6.18-dirtyfrag to manually re-run the hooks.
xbps-src needs space. The masterdir/ chroot grows to ~10GB. The kernel build adds another ~10GB. Make sure /var/cache/xbps and your void-packages checkout location have enough room.
Hostdir vs masterdir. Sources go to hostdir/sources/, builds happen in masterdir/, output goes to hostdir/binpkgs/. Don't confuse them.
Same as Phase 5 (Void glibc) with one critical difference: builds happen in a musl chroot. The kernel binary itself is libc-independent (it doesn't link against any libc), but the userspace tooling around the kernel build differs.
cat /etc/os-release
# NAME="Void"
# ID=void
ldd --version 2>&1 | head -1
# musl libc
xbps-query -p version musl 2>/dev/null
# musl-1.2.5_1 (or similar)Everything in Phase 5 applies, except the xbps-src invocation needs -A x86_64-musl:
# Bootstrap a musl masterdir
./xbps-src -A x86_64-musl binary-bootstrap
# Fetch
./xbps-src -A x86_64-musl fetch linux6.18
# Build
./xbps-src -A x86_64-musl pkg linux6.18 2>&1 | tee build.logWithout -A x86_64-musl, xbps-src builds a glibc binary that won't run on musl Void.
sudo xbps-install --repository=$(pwd)/hostdir/binpkgs/ \
-S linux6.18 linux6.18-headers(Note: binpkgs/ for musl, not binpkgs/x86_64/ — musl uses a flat layout.)
mkinitfs not mkinitcpio. Void musl uses mkinitfs (or dracut in newer setups) to build initramfs. Don't blindly copy CachyOS/Arch habits.
Some kernel-related userspace assumes glibc. perf, bpftool, bpftrace all build on musl but with reduced feature sets. Verification via bpftrace (Section 13) may have rough edges.
Smaller kernel packages. Musl kernel packages are typically 10-15% smaller because the userspace utilities packaged alongside them are smaller. The kernel binary itself is identical to glibc Void.
Cross-libc package set is limited. Most kernel-related deps exist on musl Void, but if the script complains about a missing dep, check if a -musl-specific package exists.
The kernel binary is libc-independent. This means a kernel package built on glibc Void will technically boot on musl Void, but the userspace tools (udev, mkinitfs triggers, etc.) won't work. Always build with -A x86_64-musl for musl targets.
If you run containers (Podman, Docker, LXC, systemd-nspawn) on any of the above distros, the host kernel and host page cache are shared with all containers. A container exploit corrupts the HOST. Patching only the host (or only the containers) is incomplete.
ESP variant in a container: attacker runs unshare(CLONE_NEWUSER) inside the container to get CAP_NET_ADMIN, registers an XFRM Security Association, sends a UDP packet, the host kernel processes it, the host page cache gets corrupted. They can now overwrite /usr/bin/su on the HOST, then execute it from inside the container if the container has access to /usr/bin/su (which most don't), OR escape using a different vector and use the now-corrupted host binary.
RxRPC variant: doesn't need user namespaces. Just socket(AF_RXRPC). Container restrictions on capabilities don't help.
Layer 1: Host module blacklist (mandatory). Already done in Phase 0. This blocks the attack at the only point it can be exploited — the kernel module load.
Layer 2: Block user namespaces (breaks rootless tools).
# Block all user namespaces
sudo sysctl -w user.max_user_namespaces=0
echo "user.max_user_namespaces = 0" | sudo tee /etc/sysctl.d/99-dirtyfrag.conf
sudo sysctl --system
# Or, only block unprivileged user namespaces (Debian/Ubuntu specific)
sudo sysctl -w kernel.unprivileged_userns_clone=0This blocks the ESP attack vector (which uses unshare(CLONE_NEWUSER) for CAP_NET_ADMIN). It does NOT block RxRPC. Trade-offs:
- ❌ Rootless Podman / Docker stops working
- ❌ Flatpak's bubblewrap sandboxing breaks
- ❌ Chrome/Chromium sandbox falls back to setuid mode (or fails)
- ❌
unshare -Ufor general process isolation breaks - ❌
firejailpartial sandbox features break
Layer 3: Harden BPF JIT.
sudo sysctl -w net.core.bpf_jit_harden=2
echo "net.core.bpf_jit_harden = 2" | sudo tee -a /etc/sysctl.d/99-dirtyfrag.confThis makes BPF JIT-compiled code harder to use as gadgets. Doesn't directly prevent Dirty Frag but raises the bar for chained exploits.
Layer 4: Seccomp profile to deny xfrm syscalls.
For Podman/Docker, edit your seccomp profile (typically at /usr/share/containers/seccomp.json or specified in --security-opt seccomp=...):
{
"names": [
"xfrm_state_add",
"xfrm_policy_add",
"xfrm_state_modify",
"xfrm_policy_modify"
],
"action": "SCMP_ACT_ERRNO",
"args": [],
"comment": "Block xfrm syscalls (Dirty Frag mitigation)"
}Add this to the syscalls array in your seccomp profile. New containers using this profile will get EPERM when they try the relevant syscalls.
Layer 5: AppArmor/SELinux.
For AppArmor (Ubuntu, Debian, openSUSE):
# In /etc/apparmor.d/abstractions/dirtyfrag-mitigation
deny capability net_admin,
deny @{PROC}/net/xfrm_** rw,
deny @{PROC}/sys/net/core/xfrm** rw,
Then include in your container profile and reload:
sudo apparmor_parser -r /etc/apparmor.d/abstractions/dirtyfrag-mitigation
sudo systemctl restart apparmorFor SELinux: write a custom policy module that denies xfrm_socket to your container types. (Out of scope for this guide, but the manpage xfrm_selinux(8) is the starting point.)
A multi-agent Claude orchestration system running containers on a Void glibc host. The containers share the host kernel.
# 1. Apply host module blacklist (Phase 0)
sudo tee /etc/modprobe.d/dirtyfrag-mitigation.conf > /dev/null << 'EOF'
install esp4 /bin/false
install esp6 /bin/false
install rxrpc /bin/false
install ipcomp4 /bin/false
install ipcomp6 /bin/false
install xfrm_user /bin/false
EOF
# Unload anything running
for m in esp4 esp6 rxrpc ipcomp4 ipcomp6 xfrm_user; do
sudo modprobe -r "$m" 2>/dev/null || true
done
# Update initramfs (Void uses dracut on some setups)
if command -v dracut &>/dev/null; then
sudo dracut --force
elif command -v mkinitfs &>/dev/null; then
sudo mkinitfs
fi
# 2. Decide on user namespaces (RALPH probably uses rootful Podman)
# If RALPH uses rootless: skip this step (would break it)
# If RALPH uses rootful (recommended for orchestration): block userns
sudo sysctl -w user.max_user_namespaces=0
echo "user.max_user_namespaces = 0" | sudo tee /etc/sysctl.d/99-dirtyfrag.conf
# 3. Harden BPF
sudo sysctl -w net.core.bpf_jit_harden=2
echo "net.core.bpf_jit_harden = 2" | sudo tee -a /etc/sysctl.d/99-dirtyfrag.conf
# 4. Seccomp deny in podman config
# Edit /usr/share/containers/seccomp.json or RALPH's container security context
# to add the xfrm_state_add deny rule shown in 12.2
# 5. Verify
sudo modprobe -n -v esp4
# Expected: install /bin/false
sudo sysctl user.max_user_namespaces
# Expected: user.max_user_namespaces = 0
# 6. Restart RALPH containers to pick up new seccomp
podman ps -q | xargs -r podman restartThe RALPH host now blocks both vulnerabilities at multiple layers.
Three methods, in decreasing order of reliability.
This actually proves the patched code is executing on the running kernel. No theory, no source-code grepping — runtime evidence.
# Install bpftrace if needed
sudo apt install bpftrace # Ubuntu
sudo pacman -S bpf # CachyOS / Arch
sudo apk add bpftrace # Alpine
sudo xbps-install -Sy bpftrace # Void
# Run the probe
sudo bpftrace -e '
kfunc:vmlinux:esp_input {
@esp_calls += 1;
}
kfunc:vmlinux:skb_cow_data /pid != 0/ {
@cow_calls += 1;
@cow_stacks[kstack(5)] = count();
}
' &
BPF_PID=$!
# Generate IPsec traffic if you have a tunnel
# Or just wait for ambient traffic / scheduled tasks
sleep 120
sudo kill $BPF_PIDExpected output looks like:
@esp_calls: 47
@cow_calls: 31
@cow_stacks[
skb_cow_data+1
esp_input+524
xfrm_input+862
xfrm4_rcv_encap+115
udp_queue_rcv_skb+356
]: 12
The presence of esp_input → skb_cow_data paths in the stack traces means the patched code is doing the cow when needed. If you only see esp_input calls but no skb_cow_data, the cow is being skipped — your kernel may not have the patch.
This proves the patched bytecode is in the kernel binary.
# Find vmlinux
VMLINUX=""
for path in \
"/usr/lib/modules/$(uname -r)/build/vmlinux" \
"/usr/lib/debug/boot/vmlinux-$(uname -r)" \
"/boot/vmlinuz-$(uname -r)"; do
[[ -f "$path" ]] && { VMLINUX="$path"; break; }
done
echo "Using: $VMLINUX"
# Disassemble esp_input and look for the new flag-test instructions
sudo objdump -d "$VMLINUX" 2>/dev/null \
| awk '/<esp_input>:/,/^esp_input_done|^[a-f0-9]+ <[a-z_]/' \
| grep -E 'test|and|cmp' | head -20Expected output:
ffffffff81a1234e: 48 8b 47 50 mov 0x50(%rdi),%rax
ffffffff81a12352: f6 c4 01 test $0x1,%ah # SKBFL_SHARED_FRAG check
ffffffff81a12355: 75 1c jne ffffffff81a12373 # if set, take cow path
ffffffff81a12357: 48 8b 47 28 mov 0x28(%rdi),%rax
ffffffff81a1235b: 48 85 c0 test %rax,%rax # data_len check
ffffffff81a1235e: 75 13 jne ffffffff81a12373 # if non-zero, cow
If you see test $0x1, ... instructions in esp_input near the original skb_has_frag_list jump target, the flag check is in the binary. If the disassembly shows just a single test with no flag-related comparison after the skb_has_frag_list call, the patch isn't there.
This only proves the source had the patch, not that the running kernel was built from that source. Use it as a sanity check, not a final verification.
# Generic
sudo find /usr/src -name 'esp4.c' -exec grep -l skb_has_shared_frag {} \;
# CachyOS
ls /usr/src/linux-cachyos*/net/ipv4/esp4.c 2>/dev/null \
| xargs -r grep -l skb_has_shared_frag
# Ubuntu
ls /usr/src/linux-headers-*/net/ipv4/esp4.c 2>/dev/null \
| xargs -r grep -l skb_has_shared_frag
# Alpine
ls /usr/src/linux-*/net/ipv4/esp4.c 2>/dev/null \
| xargs -r grep -l skb_has_shared_frag
# Void
ls /usr/src/linux*-headers/net/ipv4/esp4.c 2>/dev/null \
| xargs -r grep -l skb_has_shared_fragThese are common false-friends. None of them reliably prove anything:
- ❌
grep skb_has_shared_frag /proc/kallsyms—skb_has_shared_frag()is astatic inlinefunction. The compiler inlines it directly into its callers. It NEVER appears inkallsymsregardless of patch state. - ❌
dmesg | grep esp— the patched code doesn't emit any log message. dmesg is silent on patch state. - ❌ Trusting the kernel changelog — distros sometimes silently skip backports. Always verify with method 1 or 2.
- ❌ Checking that the
Fixes:tags appear in source — those are metadata, not a runtime check. They prove someone wrote a patch, not that it was applied.
After verifying the kernel is patched:
# Is the blacklist still in place?
ls -la /etc/modprobe.d/dirtyfrag-mitigation.conf
# Is it active?
sudo modprobe -n -v esp4
# Expected: install /bin/false
# Once you've VERIFIED the patch is in, remove the blacklist:
sudo rm /etc/modprobe.d/dirtyfrag-mitigation.conf
# Distro-specific persistence updates:
sudo update-initramfs -u # Ubuntu/Debian
sudo mkinitcpio -P # Arch/CachyOS
sudo mkinitfs && [[ -f /etc/lbu/lbu.conf ]] && sudo lbu commit -d # Alpine
sudo dracut --force # Void (if using dracut)When your distro ships an official patched kernel through normal updates, you can revert to stock and remove your custom build.
# Pull official patched kernel
sudo pacman -Syu
# Verify the patch is in (Section 13)
sudo bpftrace -e 'kfunc:vmlinux:skb_cow_data { @[comm] = count(); }' &
sleep 30; sudo killall bpftrace
# Remove your custom kernel
sudo pacman -R linux-cachyos-dirtyfrag linux-cachyos-dirtyfrag-headers
# Update bootloader
if [[ -d /boot/loader/entries ]]; then
sudo bootctl update
elif command -v limine-update &>/dev/null; then
sudo limine-update
elif [[ -f /boot/grub/grub.cfg ]]; then
sudo grub-mkconfig -o /boot/grub/grub.cfg
fi
# Remove blacklist (after verification!)
sudo rm /etc/modprobe.d/dirtyfrag-mitigation.conf
sudo mkinitcpio -P
# Remove sources directory
rm -rf ~/linux-cachyos# Pull official kernel
sudo apt update && sudo apt upgrade
# Remove custom build (if you did Path C)
sudo apt remove --purge \
'linux-image-unsigned-*+dirtyfrag1*' \
'linux-headers-*+dirtyfrag1*' \
'linux-modules-*+dirtyfrag1*'
# Or remove mainline (if you did Path B)
sudo mainline list-installed
sudo mainline uninstall v7.0.4 # whichever version
# Update GRUB
sudo update-grub
# Remove blacklist (after verification!)
sudo rm /etc/modprobe.d/dirtyfrag-mitigation.conf
sudo update-initramfs -u
# Reboot to confirm official kernel boots
sudo reboot# Pull updates
sudo apk update && sudo apk upgrade
# Remove custom kernel
sudo apk del linux-lts-dirtyfrag linux-lts-dirtyfrag-dev linux-lts-dirtyfrag-doc
# Or for RPi:
sudo apk del linux-rpi-dirtyfrag linux-rpi-dirtyfrag-dev linux-rpi-dirtyfrag-doc
# Reinstall stock
sudo apk add linux-lts linux-lts-dev linux-lts-doc
# or: linux-rpi linux-rpi-dev linux-rpi-doc
# Bootloader
sudo update-extlinux 2>/dev/null || true
# RPi: revert /boot/config.txt
# sudo nano /boot/config.txt → change kernel= back
# Diskless persist
[[ -f /etc/lbu/lbu.conf ]] && sudo lbu commit -d
# Remove blacklist
sudo rm /etc/modprobe.d/dirtyfrag-mitigation.conf
[[ -f /etc/lbu/lbu.conf ]] && sudo lbu commit -d# Update
sudo xbps-install -Suy
# Remove custom kernel (if pkgname-renamed)
sudo xbps-remove -y linux6.18-dirtyfrag
# Or revert to stock revision
sudo xbps-install -f linux6.18
# Reinstall NVIDIA/ZFS for the official kernel
sudo xbps-install -Sy nvidia zfs
# Bootloader hooks
sudo xbps-reconfigure -f linux6.18
# Remove blacklist
sudo rm /etc/modprobe.d/dirtyfrag-mitigation.conf
sudo dracut --force # or mkinitfs for muslExpected on current kernels (7.0.x, 6.18.x) because the patch targets ~6.13 source. The default --fuzz=5 should handle most cases, but if hunks fail:
# Try wider fuzz first
patch -p1 --fuzz=10 < dirtyfrag-esp-fix.patch
# Look at the failed hunk in the .rej file
cat net/ipv4/esp4.c.rejIf --fuzz=10 fails, apply by hand. The changes are tiny:
# ESP esp4.c — find the line
grep -n "skb_has_frag_list" net/ipv4/esp4.c
# 875: } else if (!skb_has_frag_list(skb)) {
# Edit the file
nano net/ipv4/esp4.c
# Change line 875 from:
# } else if (!skb_has_frag_list(skb)) {
# To:
# } else if (!skb_has_frag_list(skb) &&
# !skb_has_shared_frag(skb) &&
# !skb->data_len) {
# Same for net/ipv6/esp6.c
grep -n "skb_has_frag_list" net/ipv6/esp6.c
nano net/ipv6/esp6.c
# ip_output.c — find line above wmem_alloc_delta
grep -n "wmem_alloc_delta += copy" net/ipv4/ip_output.c
# 1233: wmem_alloc_delta += copy;
# Edit and add ABOVE the wmem_alloc_delta line:
nano net/ipv4/ip_output.c
# Insert these two lines:
# if (!(flags & MSG_NO_SHARED_FRAGS))
# skb_shinfo(skb)->flags |= SKBFL_SHARED_FRAG;
# Same for ip6_output.c
grep -n "wmem_alloc_delta += copy" net/ipv6/ip6_output.c
nano net/ipv6/ip6_output.cFor RxRPC if you're applying it:
# net/rxrpc/call_event.c — find the line
grep -n "skb_cloned(skb)" net/rxrpc/call_event.c
# Change "skb_cloned(skb)" to "(skb_cloned(skb) || skb->data_len)"
grep -n "skb_cloned(skb)" net/rxrpc/conn_event.c
# Same changegrep -rn MSG_NO_SHARED_FRAGS include/
# Empty? Strip the conditional from the patch:
sed -i '/^+\s*if (!(flags & MSG_NO_SHARED_FRAGS))$/d' \
dirtyfrag-esp-fix.patchYou forgot to rebuild DKMS. From your fallback kernel:
# CachyOS
sudo dkms autoinstall
# 5060 Ti needs:
sudo pacman -S nvidia-open-dkms
# Ubuntu
sudo dpkg-reconfigure $(dpkg -l | awk '/nvidia-dkms-/{print $2}' | head -1)
# Void
sudo xbps-install -Sy nvidia# Halve parallelism
MAKEFLAGS="-j$(($(nproc) / 2))" makepkg -s --skippgpcheck # CachyOS
# Ubuntu
LANG=C fakeroot debian/rules binary-headers binary-generic binary-perarch \
skipabi=true skipmodule=true skipretpoline=true \
-j$(($(nproc) / 2))
# Cap link parallelism specifically (LTO link is the OOM hotspot)
MAKEFLAGS="-j$(nproc) LDFLAGS_vmlinux=--threads=4"If you forgot to lbu commit and rebooted, your custom kernel is gone. Rebuild and remember this time:
cd ~/aports/main/linux-lts
abuild -r
sudo apk add --allow-untrusted ~/packages/main/x86_64/linux-lts-dirtyfrag*.apk
sudo update-extlinux
sudo lbu commit -d # <-- THIS TIME
sudo reboot# Force reconfiguration of the kernel hooks
sudo xbps-reconfigure -f linux6.18-dirtyfrag
# If that doesn't help, regenerate bootloader config manually
sudo grub-mkconfig -o /boot/grub/grub.cfg
# Or for syslinux/extlinux
sudo /usr/libexec/xbps-kernel-hook.sh post-install linux6.18 6.18.26_99# Check config.txt
cat /boot/config.txt | grep -i kernel
# If it doesn't reference your custom image:
sudo nano /boot/config.txt
# Make sure the [all] (or [default]) section has:
# kernel=vmlinuz-rpi-dirtyfrag
# initramfs initramfs-rpi-dirtyfrag
# Also check the file actually exists in /boot
ls /boot/vmlinuz-rpi-dirtyfrag /boot/initramfs-rpi-dirtyfrag
sudo reboot# Confirm SB state
sudo bootctl status | grep -i 'secure boot'
mokutil --sb-state
# Two options:
# Option A: disable in firmware (easiest)
# Option B: sign with sbctl (CachyOS) or sbsigntool (Ubuntu)
# CachyOS / Arch
sudo pacman -S sbctl
sudo sbctl status
sudo sbctl create-keys # one-time
sudo sbctl enroll-keys -m # one-time, needs reboot+confirm
sudo sbctl sign -s /boot/vmlinuz-linux-cachyos-dirtyfrag
# Ubuntu
sudo apt install sbsigntool mokutil
# Generate MOK key/cert
openssl req -new -x509 -newkey rsa:2048 -keyout MOK.priv -out MOK.pem \
-nodes -days 36500 -subj "/CN=Local Custom Kernel"
sudo mokutil --import MOK.pem # reboot+confirm in MOK Manager
sudo sbsign --key MOK.priv --cert MOK.pem \
--output /boot/vmlinuz-7.0.0-12-generic+dirtyfrag1 \
/boot/vmlinuz-7.0.0-12-generic+dirtyfrag1Kexec under Secure Boot is unreliable. kexec_load() is typically blocked by lockdown mode; kexec_file_load() requires the new kernel to be signed by a key the firmware trusts.
# Just reboot
sudo reboot
# If you really want to try kexec:
sudo kexec --type=Image-bzImage \
-l /boot/vmlinuz-... \
--initrd=/boot/initramfs-... \
--reuse-cmdline
sudo systemctl kexec
# (systemctl kexec is cleaner than `kexec -e` because it runs shutdown hooks)ccache -C # nuke cache
export CCACHE_NOCOMPRESS=1
export CCACHE_SLOPPINESS=time_macros,include_file_mtime,include_file_ctime
# Pin build timestamp so it doesn't bust cache on every build
export KBUILD_BUILD_TIMESTAMP="$(date -u -d @$(git log -1 --format=%ct) '+%Y-%m-%dT%H:%M:%SZ')"The most common case for May 2026+ readers.
# t=0min — verify what we're on
$ uname -r
7.0.3-1-cachyos
# t=1min — apply Phase 0 mitigation as defense-in-depth while we check
$ sudo tee /etc/modprobe.d/dirtyfrag-mitigation.conf > /dev/null << 'EOF'
> install esp4 /bin/false
> install esp6 /bin/false
> install rxrpc /bin/false
> install ipcomp4 /bin/false
> install ipcomp6 /bin/false
> install xfrm_user /bin/false
> EOF
$ sudo modprobe -r esp4 esp6 rxrpc ipcomp4 ipcomp6 xfrm_user 2>/dev/null
# t=3min — clone the kernel repo, fetch source for inspection
$ cd ~
$ git clone --depth 1 https://github.com/CachyOS/linux-cachyos.git
$ cd linux-cachyos/linux-cachyos
$ makepkg -o --skippgpcheck
==> Making package: linux-cachyos 7.0.3-1
==> Retrieving sources...
-> Downloading linux-7.0.3.tar.xz...
==> Validating source files with b2sums...
==> Extracting sources...
# t=5min — check if patch is in
$ grep -A 4 "skb_has_frag_list(skb)" src/linux-7.0.3/net/ipv4/esp4.c | head
} else if (!skb_has_frag_list(skb) &&
!skb_has_shared_frag(skb)) {
nfrags = skb_shinfo(skb)->nr_frags;
nfrags++;
# Patch is in! No build needed. Verify.
# t=6min — install bpftrace
$ sudo pacman -S bpf
# t=7min — runtime probe
$ sudo bpftrace -e '
> kfunc:vmlinux:esp_input { @esp_calls += 1; }
> kfunc:vmlinux:skb_cow_data /pid != 0/ { @cow_calls += 1; @stk[kstack(5)] = count(); }
> ' &
[1] 14523
# Generate ambient traffic / scheduled tasks for 60 sec
$ sleep 60
$ sudo kill %1
@esp_calls: 12
@cow_calls: 8
@stk[
skb_cow_data+1
esp_input+524
xfrm_input+862
...
]: 3
# t=8min — patched kernel verified. Remove blacklist.
$ sudo rm /etc/modprobe.d/dirtyfrag-mitigation.conf
$ sudo mkinitcpio -P
# Done. Total time: 8 minutes.Less common but illustrative for someone who needs IPsec and hasn't gotten the SRU yet.
# t=0min
$ uname -r
7.0.0-12-generic
$ cat /etc/os-release | head -3
PRETTY_NAME="Ubuntu 26.04 LTS"
NAME="Ubuntu"
VERSION_ID="26.04"
# t=1min — Phase 0 mitigation
$ sudo tee /etc/modprobe.d/dirtyfrag-mitigation.conf > /dev/null << 'EOF'
> install esp4 /bin/false
> install esp6 /bin/false
> install rxrpc /bin/false
> install ipcomp4 /bin/false
> install ipcomp6 /bin/false
> install xfrm_user /bin/false
> EOF
$ sudo update-initramfs -u
# t=3min — install build deps
$ sudo apt update
$ sudo apt install -y fakeroot dpkg-dev libelf-dev libssl-dev \
> libncurses-dev bison flex bc rsync zstd dwarves git
$ sudo apt build-dep -y linux linux-image-unsigned-$(uname -r)
# t=8min — enable deb-src
$ sudo sed -i 's/^Types: deb$/Types: deb deb-src/' \
> /etc/apt/sources.list.d/ubuntu.sources
$ sudo apt update
# t=10min — clone Launchpad source
$ mkdir -p ~/ubuntu-kernel
$ cd ~/ubuntu-kernel
$ git clone --depth 1 \
> git://git.launchpad.net/~ubuntu-kernel/ubuntu/+source/linux/+git/resolute \
> -b master-next
$ cd resolute
# t=15min — pre-flight check
$ grep -A 4 "skb_has_frag_list(skb)" net/ipv4/esp4.c | head
} else if (!skb_has_frag_list(skb)) {
nfrags = skb_shinfo(skb)->nr_frags;
# Vulnerable. Continue with patch.
$ grep -rn MSG_NO_SHARED_FRAGS include/
include/linux/socket.h:354: #define MSG_NO_SHARED_FRAGS 0x100000
# OK, symbol exists, patch applies as-is.
# t=16min — apply patch
$ patch -p1 --fuzz=5 < ~/dirtyfrag-patches/dirtyfrag-esp-fix.patch
patching file net/ipv4/esp4.c
Hunk #1 succeeded at 875 with fuzz 2 (offset 2 lines).
patching file net/ipv4/ip_output.c
Hunk #1 succeeded at 1241 (offset 8 lines).
patching file net/ipv6/esp6.c
Hunk #1 succeeded at 919 with fuzz 2 (offset 4 lines).
patching file net/ipv6/ip6_output.c
Hunk #1 succeeded at 1802 (offset 8 lines).
# t=17min — set version suffix and configure
$ sed -i '0,/^linux (/{s/^linux (\([^)]*\))/linux (\1+dirtyfrag1)/}' \
> debian.master/changelog
$ head -1 debian.master/changelog
linux (7.0.0-12.13+dirtyfrag1) resolute; urgency=medium
$ chmod +x debian/rules debian/scripts/* debian/scripts/misc/*
$ fakeroot debian/rules clean
$ scripts/config --set-str SYSTEM_TRUSTED_KEYS ""
$ scripts/config --set-str SYSTEM_REVOCATION_KEYS ""
# t=20min — build (long)
$ LANG=C fakeroot debian/rules binary-headers binary-generic binary-perarch \
> skipabi=true skipmodule=true skipretpoline=true \
> -j$(nproc) 2>&1 | tee build.log
... (45 minutes pass) ...
# t=65min — install
$ cd ..
$ ls *.deb
linux-headers-7.0.0-12_*+dirtyfrag1*_all.deb
linux-headers-7.0.0-12-generic_*+dirtyfrag1*_amd64.deb
linux-image-unsigned-7.0.0-12-generic_*+dirtyfrag1*_amd64.deb
linux-modules-7.0.0-12-generic_*+dirtyfrag1*_amd64.deb
$ sudo dpkg -i \
> linux-headers-*+dirtyfrag1*.deb \
> linux-image-unsigned-*+dirtyfrag1*.deb \
> linux-modules-*+dirtyfrag1*.deb
# t=68min — DKMS rebuild
$ nv_pkg=$(dpkg -l | awk '/nvidia-dkms-/{print $2}' | head -1)
$ sudo dpkg-reconfigure "$nv_pkg"
$ sudo update-grub
$ sudo systemctl restart apparmor
# t=70min — reboot
$ sudo reboot
# After reboot:
$ uname -r
7.0.0-12-generic+dirtyfrag1
# Verify
$ sudo bpftrace -e '...' & # see Section 13.1
... evidence of skb_cow_data calls ...
$ sudo rm /etc/modprobe.d/dirtyfrag-mitigation.conf
$ sudo update-initramfs -u
# Done. Total: ~75 minutes.# t=0min — verify environment
$ uname -m
aarch64
$ cat /proc/device-tree/model | tr -d '\0'
Raspberry Pi 5 Model B Rev 1.0
$ uname -r
6.18.26-0-rpi
$ cat /etc/alpine-release
3.23.2
# t=1min — Phase 0 mitigation (and lbu commit immediately, since diskless)
$ sudo tee /etc/modprobe.d/dirtyfrag-mitigation.conf > /dev/null << 'EOF'
> install esp4 /bin/false
> install esp6 /bin/false
> install rxrpc /bin/false
> install ipcomp4 /bin/false
> install ipcomp6 /bin/false
> install xfrm_user /bin/false
> EOF
$ grep "tmpfs / " /proc/mounts && sudo lbu commit -d
tmpfs / tmpfs ...
[lbu output...]
# t=3min — set up build
$ sudo apk add alpine-sdk build-base bc bison flex elfutils-dev openssl-dev \
> linux-headers perl python3 diffutils findutils xz zstd tar git sed coreutils
$ sudo addgroup $(whoami) abuild
$ newgrp abuild
$ abuild-keygen -a -n
$ sudo cp ~/.abuild/*.rsa.pub /etc/apk/keys/
$ sudo chmod 644 /etc/apk/keys/*.rsa.pub
# t=8min — clone aports
$ git clone --depth 1 --branch 3.23-stable \
> https://gitlab.alpinelinux.org/alpine/aports.git ~/aports
$ cd ~/aports/main/linux-rpi
# t=10min — pre-flight check
$ abuild fetch
$ abuild unpack
$ grep -A 4 "skb_has_frag_list(skb)" \
> src/linux-6.18.26/net/ipv4/esp4.c | head
} else if (!skb_has_frag_list(skb)) {
# Vulnerable, continue.
# t=12min — stage patch and edit APKBUILD
$ cp ~/dirtyfrag-patches/dirtyfrag-esp-fix.patch .
$ nano APKBUILD
# Add _flavor="rpi-dirtyfrag" near top
# Add dirtyfrag-esp-fix.patch to source=
# Add patch invocation in prepare()
$ abuild checksum
# t=15min — build (long on Pi 5)
$ abuild -r 2>&1 | tee build.log
... (45 minutes on Pi 5 with NVMe) ...
# t=60min — install
$ ls ~/packages/main/aarch64/linux-rpi-dirtyfrag*.apk
linux-rpi-dirtyfrag-6.18.26-r0.apk
linux-rpi-dirtyfrag-dev-6.18.26-r0.apk
linux-rpi-dirtyfrag-doc-6.18.26-r0.apk
$ sudo apk add --allow-untrusted \
> ~/packages/main/aarch64/linux-rpi-dirtyfrag*.apk
# t=63min — Pi-specific bootloader update
$ ls /boot/ | grep dirtyfrag
vmlinuz-rpi-dirtyfrag
initramfs-rpi-dirtyfrag
$ cat /boot/config.txt
[all]
arm_64bit=1
kernel=vmlinuz-rpi
initramfs initramfs-rpi
include usercfg.txt
$ sudo nano /boot/config.txt
# Change to:
# [all]
# arm_64bit=1
# kernel=vmlinuz-rpi-dirtyfrag
# initramfs initramfs-rpi-dirtyfrag
# include usercfg.txt
# t=65min — diskless persist!
$ sudo lbu commit -d
$ sudo reboot
# After reboot:
$ uname -r
6.18.26-0-rpi-dirtyfrag
# Verify (Section 13.1 / 13.3)
$ sudo find /usr/src -name esp4.c -exec grep -l skb_has_shared_frag {} \;
/usr/src/linux-headers-6.18.26-0-rpi-dirtyfrag/net/ipv4/esp4.c
$ sudo rm /etc/modprobe.d/dirtyfrag-mitigation.conf
$ sudo lbu commit -d
# Done.ABI — Application Binary Interface. The contract between compiled binaries and the kernel (system call numbers, struct layouts, etc.). Ubuntu's skipabi=true disables enforcement of this for non-official builds.
APKBUILD — Alpine's package build recipe, similar to PKGBUILD or rpm's spec file.
bpftrace — A high-level eBPF tracing language. Used for runtime kernel probing.
Cc: stable — A tag in upstream commit messages indicating the patch should be backported to all active stable kernel trees. The stable kernel team's automation handles this.
DKMS — Dynamic Kernel Module Support. Framework for rebuilding out-of-tree kernel modules (NVIDIA, ZFS, VirtualBox) when the kernel is upgraded.
EEVDF — Enhanced Earliest Virtual Deadline First. The default Linux scheduler since 6.6. CachyOS uses BORE on top of it for desktop responsiveness.
ESP — Encapsulating Security Payload. The IPsec protocol that provides encryption and authentication. CVE-2026-43284 is in esp_input().
Fuzz / Offset — When patch applies a hunk and the surrounding context doesn't exactly match (because line numbers shifted), it tries to find the right location with some flexibility. "Fuzz" is how many context lines it's allowed to ignore. "Offset" is by how many lines the hunk moved.
kexec — A syscall that lets one Linux kernel "boot" another without a full firmware reset. Useful for fast iteration.
LBU — Local Backup Utility. Alpine's tool for persisting /etc changes to /boot in diskless mode.
Limine — A modern bootloader, recently adopted as default by CachyOS.
LTO — Link-Time Optimization. Compiler optimization that operates on the whole program at link time. Produces faster code at the cost of much higher RAM use and longer build times.
MOK — Machine Owner Key. Under Secure Boot, the firmware's allow-list of trusted keys. mokutil manages it.
Page Cache — RAM kernel-managed cache of file contents. The target of Dirty Frag.
pkgrel / pkgbase / pkgname — Arch package versioning. pkgrel is the local revision (bumped for rebuilds). pkgbase is the upstream package name. pkgname is what gets installed.
runit — Void Linux's init system. Replaces systemd.
SKB / sk_buff — Socket Buffer. The kernel's representation of a network packet.
SKBFL_SHARED_FRAG — A flag in skb_shared_info->flags indicating the skb's frags[] contain pages from a shared/external source. Set in patched __ip_append_data(), checked in patched esp_input().
splice() — A Linux syscall that moves data between file descriptors without copying through userspace. The bridge that enables Dirty Frag.
SRU — Stable Release Update. Canonical's process for delivering kernel updates to released Ubuntu versions.
xbps / xbps-src — Void Linux's package manager and build system.
XFRM — Linux's IPsec framework. xfrm_state_add is the syscall that registers a new Security Association.
- Upstream ESP commit:
f4c50a4034e6— Kuan-Ting Chen, "esp,ip: fix page-cache write via splice + in-place crypto", merged late April 2026 withCc: stable. - RxRPC patch: Hyunwoo Kim, message-id
<afKV2zGR6rrelPC7@v4bel>, submitted to netdev 2026-04-29, unmerged as of writing. - Original Kim v1 ESP patch: message-id
<afLDKSvAvMwGh7Fy@v4bel>, April 30 2026, superseded by Chen's v2. - CachyOS kernel repo:
https://github.com/CachyOS/linux-cachyos - CachyOS kernel-patches repo:
https://github.com/CachyOS/kernel-patches - CachyOS wiki kernel page:
https://wiki.cachyos.org/features/kernel/ - Alpine aports:
https://gitlab.alpinelinux.org/alpine/aports - Alpine Custom Kernel wiki:
https://wiki.alpinelinux.org/wiki/Custom_Kernel - Alpine APKBUILD reference:
https://wiki.alpinelinux.org/wiki/APKBUILD_Reference - Ubuntu kernel team docs:
https://canonical-kernel-docs.readthedocs-hosted.com/ - Ubuntu mainline builds:
https://kernel.ubuntu.com/mainline/ - Ubuntu Launchpad kernel git (26.04 =
resolute):git://git.launchpad.net/~ubuntu-kernel/ubuntu/+source/linux/+git/resolute - Void packages:
https://github.com/void-linux/void-packages - Void documentation:
https://docs.voidlinux.org/ - Stable kernel trees:
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git - Hyunwoo Kim's Theori publications:
https://theori.io/ - Phoronix CachyOS benchmarks:
https://www.phoronix.com/ - Raspberry Pi config.txt reference:
https://www.raspberrypi.com/documentation/computers/config_txt.html - Linux kernel Documentation/networking/skbuff.rst for SKB internals
- Linux kernel Documentation/networking/xfrm/index.rst for XFRM/IPsec architecture
- CVE-2022-0847 (Dirty Pipe) — predecessor of this bug class
- These instructions are best-effort. Always read what commands do before running with
sudo. - Each step should be logged. The provided helper scripts log to
/tmp/dirtyfrag-<distro>-<timestamp>.log— keep these for forensics and audit. - The local hardened ESP patch (with
!skb->data_len) is stronger than upstream. When you switch back to the stock kernel after distros ship the official fix, you lose thedata_lencheck but keep upstream'sSKBFL_SHARED_FRAGcheck, which is sufficient for the known attack vectors. - If this guide is older than 60 days from the date at the top, upstream has likely shipped a complete fix — check distro changelogs before rebuilding.
- The RxRPC patch is unmerged upstream at the time of writing. If you must run kAFS, watch netdev for the maintainer-blessed version and switch to it when it lands.
- For a kernel as critical as the production system you depend on: build twice, test once, reboot once, verify always. There is no shame in being slow when the cost of an error is a non-booting workstation.
Document ends. Patches in §2.1 and §2.2. Worked examples in §16. Verification in §13.
— Compiled 2026-05-08, Toronto.